rulesync 9.0.2 → 9.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  /**
@@ -6491,6 +7142,12 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
6491
7142
  relativeFilePath: CODEXCLI_HOOKS_FILE_NAME
6492
7143
  };
6493
7144
  }
7145
+ static getExtraSharedWritePaths() {
7146
+ return [{
7147
+ relativeDirPath: CODEXCLI_DIR,
7148
+ relativeFilePath: CODEXCLI_MCP_FILE_NAME
7149
+ }];
7150
+ }
6494
7151
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
6495
7152
  const paths = CodexcliHooks.getSettablePaths({ global });
6496
7153
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
@@ -6723,6 +7380,20 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6723
7380
  }
6724
7381
  };
6725
7382
  //#endregion
7383
+ //#region src/utils/object.ts
7384
+ /**
7385
+ * Return a shallow copy of `obj` keeping only the entries whose value is
7386
+ * neither `undefined` nor `null`.
7387
+ *
7388
+ * Used to assemble generated config objects without one conditional spread per
7389
+ * optional field (which would otherwise exceed the lint complexity budget).
7390
+ */
7391
+ function compact(obj) {
7392
+ const result = {};
7393
+ for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) result[key] = value;
7394
+ return result;
7395
+ }
7396
+ //#endregion
6726
7397
  //#region src/features/hooks/copilotcli-hooks.ts
6727
7398
  /**
6728
7399
  * GitHub Copilot CLI hooks.
@@ -6734,7 +7405,7 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6734
7405
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
6735
7406
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
6736
7407
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
6737
- * `notification`). Each entry supports three hook types:
7408
+ * `notification`, `preMcpToolCall`). Each entry supports three hook types:
6738
7409
  *
6739
7410
  * - `command` — the `bash` / `powershell` command-field shape with optional
6740
7411
  * `timeoutSec`, plus optional `cwd` / `env`.
@@ -6847,20 +7518,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
6847
7518
  } else if (hookType === "http") entries.push({
6848
7519
  type: "http",
6849
7520
  ...matcherPart,
6850
- ...def.url !== void 0 && def.url !== null && { url: def.url },
7521
+ ...compact({
7522
+ url: def.url,
7523
+ headers: def.headers,
7524
+ allowedEnvVars: def.allowedEnvVars
7525
+ }),
7526
+ ...timeoutPart,
7527
+ ...rest
7528
+ });
7529
+ else entries.push({
7530
+ type: "command",
7531
+ ...matcherPart,
7532
+ ...compact({
7533
+ [commandField]: def.command,
7534
+ env: def.env
7535
+ }),
6851
7536
  ...timeoutPart,
6852
7537
  ...rest
6853
7538
  });
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
7539
  }
6865
7540
  return entries;
6866
7541
  }
@@ -7269,148 +7944,63 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
7269
7944
  };
7270
7945
  //#endregion
7271
7946
  //#region src/features/hooks/devin-hooks.ts
7947
+ const DEVIN_CONVERTER_CONFIG = {
7948
+ supportedEvents: DEVIN_HOOK_EVENTS,
7949
+ canonicalToToolEventNames: CANONICAL_TO_DEVIN_EVENT_NAMES,
7950
+ toolToCanonicalEventNames: DEVIN_TO_CANONICAL_EVENT_NAMES,
7951
+ projectDirVar: "",
7952
+ supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
7953
+ noMatcherEvents: /* @__PURE__ */ new Set([
7954
+ "sessionStart",
7955
+ "sessionEnd",
7956
+ "stop",
7957
+ "beforeSubmitPrompt"
7958
+ ])
7959
+ };
7272
7960
  /**
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).
7961
+ * Hooks generator for Devin Local (native `.devin/` hooks).
7328
7962
  *
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.
7963
+ * Devin Local adopts a Claude-Code-style lifecycle hooks model: each event maps
7964
+ * to an array of `{ matcher?, hooks: [{ type, command|prompt, timeout? }] }`
7965
+ * matcher groups.
7352
7966
  *
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).
7967
+ * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
7968
+ * level IS the event map (no wrapper key).
7969
+ * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
7970
+ * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
7971
+ * features, so reads and writes merge into the existing JSON and the file is
7972
+ * never deleted in global mode.
7387
7973
  *
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.
7974
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
7393
7975
  */
7394
7976
  var DevinHooks = class DevinHooks extends ToolHooks {
7395
7977
  constructor(params) {
7396
7978
  super({
7397
7979
  ...params,
7398
- fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
7980
+ fileContent: params.fileContent ?? "{}"
7399
7981
  });
7400
7982
  }
7983
+ /**
7984
+ * The project standalone `hooks.v1.json` is owned wholesale by rulesync and
7985
+ * may be deleted as an orphan. The global `config.json` is shared with the MCP
7986
+ * and permissions features, so it must never be deleted.
7987
+ */
7988
+ isDeletable() {
7989
+ return this.getRelativeFilePath() !== DEVIN_CONFIG_FILE_NAME;
7990
+ }
7401
7991
  static getSettablePaths({ global = false } = {}) {
7402
7992
  if (global) return {
7403
- relativeDirPath: CODEIUM_WINDSURF_DIR,
7404
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
7993
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
7994
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
7405
7995
  };
7406
7996
  return {
7407
- relativeDirPath: WINDSURF_DIR,
7408
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
7997
+ relativeDirPath: DEVIN_DIR,
7998
+ relativeFilePath: DEVIN_HOOKS_V1_FILE_NAME
7409
7999
  };
7410
8000
  }
7411
8001
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
7412
8002
  const paths = DevinHooks.getSettablePaths({ global });
7413
- const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} });
8003
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
7414
8004
  return new DevinHooks({
7415
8005
  outputRoot,
7416
8006
  relativeDirPath: paths.relativeDirPath,
@@ -7421,31 +8011,32 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7421
8011
  }
7422
8012
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
7423
8013
  const paths = DevinHooks.getSettablePaths({ global });
7424
- await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ hooks: {} }, null, 2));
8014
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7425
8015
  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);
8016
+ const devinHooks = canonicalToToolHooks({
8017
+ config,
8018
+ toolOverrideHooks: config.devin?.hooks,
8019
+ converterConfig: DEVIN_CONVERTER_CONFIG,
8020
+ logger
8021
+ });
8022
+ let fileContent;
8023
+ if (global) {
8024
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8025
+ let parsedSettings;
8026
+ try {
8027
+ parsedSettings = JSON.parse(existingContent);
8028
+ } catch (error) {
8029
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
7445
8030
  }
7446
- if (objects.length > 0) devinHooks[devinEvent] = objects;
8031
+ const merged = {
8032
+ ...isRecord(parsedSettings) ? parsedSettings : {},
8033
+ hooks: devinHooks
8034
+ };
8035
+ fileContent = JSON.stringify(merged, null, 2);
8036
+ } else {
8037
+ await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8038
+ fileContent = JSON.stringify(devinHooks, null, 2);
7447
8039
  }
7448
- const fileContent = JSON.stringify({ hooks: devinHooks }, null, 2);
7449
8040
  return new DevinHooks({
7450
8041
  outputRoot,
7451
8042
  relativeDirPath: paths.relativeDirPath,
@@ -7461,22 +8052,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7461
8052
  } catch (error) {
7462
8053
  throw new Error(`Failed to parse Devin hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
7463
8054
  }
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
- }
8055
+ const hooks = toolHooksToCanonical({
8056
+ hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
8057
+ converterConfig: DEVIN_CONVERTER_CONFIG
8058
+ });
7477
8059
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
7478
8060
  version: 1,
7479
- hooks: canonicalHooks
8061
+ hooks
7480
8062
  }, null, 2) });
7481
8063
  }
7482
8064
  validate() {
@@ -7490,7 +8072,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7490
8072
  outputRoot,
7491
8073
  relativeDirPath,
7492
8074
  relativeFilePath,
7493
- fileContent: JSON.stringify({ hooks: {} }, null, 2),
8075
+ fileContent: "{}",
7494
8076
  validate: false
7495
8077
  });
7496
8078
  }
@@ -7717,6 +8299,106 @@ function mergeHermesConfig(fileContent, patch) {
7717
8299
  }
7718
8300
  //#endregion
7719
8301
  //#region src/features/hooks/hermesagent-hooks.ts
8302
+ /**
8303
+ * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
8304
+ * `post_tool_call`) and therefore carry a `matcher`. Every other supported
8305
+ * canonical event maps to a Hermes lifecycle event, which never accepts a
8306
+ * `matcher`.
8307
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8308
+ */
8309
+ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
8310
+ /**
8311
+ * Filter the shared canonical hooks to the events Hermes understands and merge
8312
+ * the `hermesagent`-specific override block on top.
8313
+ */
8314
+ function buildEffectiveHooks(config, toolOverrideHooks) {
8315
+ const supported = new Set(HERMESAGENT_HOOK_EVENTS);
8316
+ const shared = {};
8317
+ for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) shared[event] = defs;
8318
+ return {
8319
+ ...shared,
8320
+ ...toolOverrideHooks
8321
+ };
8322
+ }
8323
+ /**
8324
+ * Convert the canonical hooks config into Hermes's native
8325
+ * `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
8326
+ *
8327
+ * Only `type: "command"` canonical hooks are emitted — Hermes shell hooks run
8328
+ * via `shlex.split`/`shell=False`, so `prompt`/`http` hooks have no native
8329
+ * equivalent and are skipped (the shared `HooksProcessor` already warns about
8330
+ * unsupported hook types centrally). `matcher` is only carried through for
8331
+ * `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
8332
+ * warning, mirroring how other adapters (e.g. AugmentCode) handle
8333
+ * matcher-less lifecycle events.
8334
+ */
8335
+ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
8336
+ const effectiveHooks = buildEffectiveHooks(config, toolOverrideHooks);
8337
+ const result = {};
8338
+ for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
8339
+ const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
8340
+ if (!nativeEvent) continue;
8341
+ const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(canonicalEvent);
8342
+ const entries = [];
8343
+ for (const def of defs) {
8344
+ if ((def.type ?? "command") !== "command") continue;
8345
+ if (typeof def.command !== "string" || def.command === "") continue;
8346
+ const entry = { command: def.command };
8347
+ if (typeof def.matcher === "string" && def.matcher !== "") if (supportsMatcher) entry.matcher = def.matcher;
8348
+ else logger?.warn(`matcher "${def.matcher}" on "${canonicalEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
8349
+ if (typeof def.timeout === "number") entry.timeout = def.timeout;
8350
+ entries.push(entry);
8351
+ }
8352
+ if (entries.length > 0) result[nativeEvent] = entries;
8353
+ }
8354
+ return result;
8355
+ }
8356
+ /**
8357
+ * Reverse {@link canonicalToHermesHooks}: parse Hermes's native
8358
+ * `hooks: { <event>: [...] }` map back into a canonical event → definition[]
8359
+ * record. Native events with no canonical equivalent (`pre_verify`,
8360
+ * `transform_tool_result`, ...) are skipped since there is nothing to round
8361
+ * -trip them into.
8362
+ */
8363
+ function hermesHooksToCanonical(hooks) {
8364
+ const canonical = {};
8365
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
8366
+ for (const [nativeEvent, entries] of Object.entries(hooks)) {
8367
+ if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
8368
+ const canonicalEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent];
8369
+ if (!canonicalEvent) continue;
8370
+ const defs = [];
8371
+ for (const raw of entries) {
8372
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
8373
+ const entry = raw;
8374
+ if (typeof entry.command !== "string") continue;
8375
+ const def = {
8376
+ type: "command",
8377
+ command: entry.command
8378
+ };
8379
+ if (typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
8380
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout;
8381
+ defs.push(def);
8382
+ }
8383
+ if (defs.length > 0) canonical[canonicalEvent] = defs;
8384
+ }
8385
+ return canonical;
8386
+ }
8387
+ /**
8388
+ * Hermes Agent shell hooks.
8389
+ *
8390
+ * Hermes Agent registers shell-command hooks under the `hooks:` key of the
8391
+ * shared user config file `~/.hermes/config.yaml` (the HERMES_HOME directory;
8392
+ * global only — Hermes has no project-scoped hooks location). Hermes only runs
8393
+ * hooks declared under its fixed `VALID_HOOKS` event keys (`pre_tool_call`,
8394
+ * `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`,
8395
+ * `on_session_end`, `subagent_start`, `subagent_stop`, ...); any other key is
8396
+ * silently ignored. Generation therefore maps canonical events onto the real
8397
+ * `VALID_HOOKS` keys and merges the resulting `hooks:` block into the existing
8398
+ * config instead of overwriting it, since that file also holds other Hermes
8399
+ * settings (model, `mcp_servers`, `command_allowlist`, ...).
8400
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8401
+ */
7720
8402
  var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7721
8403
  static getSettablePaths() {
7722
8404
  return {
@@ -7736,6 +8418,9 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7736
8418
  error: null
7737
8419
  };
7738
8420
  }
8421
+ isDeletable() {
8422
+ return false;
8423
+ }
7739
8424
  shouldMergeExistingFileContent() {
7740
8425
  return true;
7741
8426
  }
@@ -7743,18 +8428,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7743
8428
  this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
7744
8429
  }
7745
8430
  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
- });
8431
+ const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8432
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8433
+ version: 1,
8434
+ hooks
8435
+ }, null, 2) });
7753
8436
  }
7754
- static fromRulesyncHooks({ outputRoot, rulesyncHooks }) {
8437
+ static fromRulesyncHooks({ outputRoot, rulesyncHooks, logger }) {
8438
+ const config = rulesyncHooks.getJson();
7755
8439
  return new HermesagentHooks({
7756
8440
  outputRoot,
7757
- fileContent: stringifyHermesConfig({ hooks: { rulesync: rulesyncHooks.getJson() } })
8441
+ fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8442
+ config,
8443
+ toolOverrideHooks: config.hermesagent?.hooks,
8444
+ logger
8445
+ }) })
7758
8446
  });
7759
8447
  }
7760
8448
  };
@@ -8224,9 +8912,8 @@ var KiroHooks = class KiroHooks extends ToolHooks {
8224
8912
  * the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
8225
8913
  * the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
8226
8914
  *
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.)
8915
+ * (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
8916
+ * {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
8230
8917
  */
8231
8918
  var KiroCliHooks = class extends KiroHooks {
8232
8919
  static getOverrideKey() {
@@ -8234,6 +8921,203 @@ var KiroCliHooks = class extends KiroHooks {
8234
8921
  }
8235
8922
  };
8236
8923
  //#endregion
8924
+ //#region src/features/hooks/kiro-ide-hooks.ts
8925
+ /**
8926
+ * One hook entry inside the Kiro IDE v1 `hooks` array.
8927
+ *
8928
+ * `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
8929
+ * imports do not drop data they do not yet understand.
8930
+ * @see https://kiro.dev/docs/hooks/types/
8931
+ */
8932
+ const KiroIdeHookActionSchema = z.union([z.looseObject({
8933
+ type: z.literal("command"),
8934
+ command: z.optional(safeString)
8935
+ }), z.looseObject({
8936
+ type: z.literal("agent"),
8937
+ prompt: z.optional(safeString)
8938
+ })]);
8939
+ const KiroIdeHookEntrySchema = z.looseObject({
8940
+ name: z.optional(z.string()),
8941
+ description: z.optional(z.string()),
8942
+ trigger: z.optional(z.string()),
8943
+ matcher: z.optional(z.string()),
8944
+ action: z.optional(KiroIdeHookActionSchema),
8945
+ timeout: z.optional(z.number()),
8946
+ enabled: z.optional(z.boolean())
8947
+ });
8948
+ const KiroIdeHooksFileSchema = z.looseObject({
8949
+ version: z.optional(z.string()),
8950
+ hooks: z.optional(z.array(KiroIdeHookEntrySchema))
8951
+ });
8952
+ /**
8953
+ * Build the Kiro IDE hook entries for a single canonical event's definitions.
8954
+ *
8955
+ * `command`-type definitions become `{ type: "command", command }` actions and
8956
+ * `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
8957
+ * types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
8958
+ * already warns about unsupported types).
8959
+ */
8960
+ function buildKiroIdeEntriesForEvent(trigger, definitions) {
8961
+ const entries = [];
8962
+ for (const def of definitions) {
8963
+ const type = def.type ?? "command";
8964
+ let action;
8965
+ if (type === "command") {
8966
+ if (def.command === void 0) continue;
8967
+ action = {
8968
+ type: "command",
8969
+ command: def.command
8970
+ };
8971
+ } else if (type === "prompt") {
8972
+ if (def.prompt === void 0) continue;
8973
+ action = {
8974
+ type: "agent",
8975
+ prompt: def.prompt
8976
+ };
8977
+ } else continue;
8978
+ entries.push({
8979
+ name: def.name ?? trigger,
8980
+ ...def.description !== void 0 && def.description !== null && { description: def.description },
8981
+ trigger,
8982
+ ...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
8983
+ action,
8984
+ ...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
8985
+ enabled: true
8986
+ });
8987
+ }
8988
+ return entries;
8989
+ }
8990
+ function canonicalToKiroIdeHooks(config) {
8991
+ const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
8992
+ const sharedHooks = {};
8993
+ for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
8994
+ const effectiveHooks = {
8995
+ ...sharedHooks,
8996
+ ...config["kiro-ide"]?.hooks
8997
+ };
8998
+ const entries = [];
8999
+ for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
9000
+ const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
9001
+ entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
9002
+ }
9003
+ return entries;
9004
+ }
9005
+ function kiroIdeHooksToCanonical(entries) {
9006
+ const canonical = {};
9007
+ for (const entry of entries) {
9008
+ if (entry.trigger === void 0 || entry.action === void 0) continue;
9009
+ const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
9010
+ if (isPrototypePollutionKey(eventName)) continue;
9011
+ const def = {};
9012
+ if (entry.action.type === "command") {
9013
+ if (!entry.action.command) continue;
9014
+ def.type = "command";
9015
+ def.command = entry.action.command;
9016
+ } else {
9017
+ if (!entry.action.prompt) continue;
9018
+ def.type = "prompt";
9019
+ def.prompt = entry.action.prompt;
9020
+ }
9021
+ if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
9022
+ if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
9023
+ if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
9024
+ if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
9025
+ (canonical[eventName] ??= []).push(def);
9026
+ }
9027
+ return canonical;
9028
+ }
9029
+ /**
9030
+ * Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
9031
+ *
9032
+ * Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
9033
+ * `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
9034
+ * `hooks` array, so rulesync emits every generated hook into one
9035
+ * `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
9036
+ * which keeps it within the single-file hooks architecture.
9037
+ *
9038
+ * This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
9039
+ * KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
9040
+ *
9041
+ * @see https://kiro.dev/docs/hooks/
9042
+ */
9043
+ var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
9044
+ constructor(params) {
9045
+ super({
9046
+ ...params,
9047
+ fileContent: params.fileContent ?? JSON.stringify({
9048
+ version: "v1",
9049
+ hooks: []
9050
+ }, null, 2)
9051
+ });
9052
+ }
9053
+ static getSettablePaths(_options = {}) {
9054
+ return {
9055
+ relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
9056
+ relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
9057
+ };
9058
+ }
9059
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9060
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9061
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
9062
+ version: "v1",
9063
+ hooks: []
9064
+ }, null, 2);
9065
+ return new KiroIdeHooks({
9066
+ outputRoot,
9067
+ relativeDirPath: paths.relativeDirPath,
9068
+ relativeFilePath: paths.relativeFilePath,
9069
+ fileContent,
9070
+ validate
9071
+ });
9072
+ }
9073
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
9074
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9075
+ const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
9076
+ const fileContent = JSON.stringify({
9077
+ version: "v1",
9078
+ hooks
9079
+ }, null, 2);
9080
+ return new KiroIdeHooks({
9081
+ outputRoot,
9082
+ relativeDirPath: paths.relativeDirPath,
9083
+ relativeFilePath: paths.relativeFilePath,
9084
+ fileContent,
9085
+ validate
9086
+ });
9087
+ }
9088
+ toRulesyncHooks() {
9089
+ let parsed;
9090
+ try {
9091
+ parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
9092
+ } catch (error) {
9093
+ throw new Error(`Failed to parse Kiro IDE hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9094
+ }
9095
+ const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
9096
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9097
+ version: 1,
9098
+ hooks
9099
+ }, null, 2) });
9100
+ }
9101
+ validate() {
9102
+ return {
9103
+ success: true,
9104
+ error: null
9105
+ };
9106
+ }
9107
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9108
+ return new KiroIdeHooks({
9109
+ outputRoot,
9110
+ relativeDirPath,
9111
+ relativeFilePath,
9112
+ fileContent: JSON.stringify({
9113
+ version: "v1",
9114
+ hooks: []
9115
+ }, null, 2),
9116
+ validate: false
9117
+ });
9118
+ }
9119
+ };
9120
+ //#endregion
8237
9121
  //#region src/features/hooks/opencode-hooks.ts
8238
9122
  var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8239
9123
  constructor(params) {
@@ -8292,6 +9176,35 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8292
9176
  //#endregion
8293
9177
  //#region src/features/hooks/qwencode-hooks.ts
8294
9178
  /**
9179
+ * Build a single Qwen Code hook object from a canonical hook definition.
9180
+ * Command-only fields (`async`/`env`/`shell`) are emitted only on command hooks
9181
+ * and http-only fields (`headers`/`allowedEnvVars`/`once`) only on http hooks,
9182
+ * matching upstream. `statusMessage` applies to both.
9183
+ * https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
9184
+ */
9185
+ function canonicalDefToQwencodeHook(def) {
9186
+ const type = def.type ?? "command";
9187
+ const isHttp = type === "http";
9188
+ const isCommand = type === "command";
9189
+ return {
9190
+ type,
9191
+ ...compact({
9192
+ command: def.command,
9193
+ url: def.url,
9194
+ timeout: def.timeout,
9195
+ name: def.name,
9196
+ description: def.description,
9197
+ statusMessage: def.statusMessage,
9198
+ async: isCommand ? def.async : void 0,
9199
+ env: isCommand ? def.env : void 0,
9200
+ shell: isCommand ? def.shell : void 0,
9201
+ headers: isHttp ? def.headers : void 0,
9202
+ allowedEnvVars: isHttp ? def.allowedEnvVars : void 0,
9203
+ once: isHttp ? def.once : void 0
9204
+ })
9205
+ };
9206
+ }
9207
+ /**
8295
9208
  * Convert canonical hooks config to Qwen Code format.
8296
9209
  * Filters shared hooks to QWENCODE_HOOK_EVENTS, merges config.qwencode?.hooks,
8297
9210
  * then converts to PascalCase and Qwen Code matcher/hooks structure.
@@ -8319,16 +9232,7 @@ function canonicalToQwencodeHooks(config) {
8319
9232
  }
8320
9233
  const entries = [];
8321
9234
  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
- });
9235
+ const hooks = defs.map(canonicalDefToQwencodeHook);
8332
9236
  const sequential = defs.some((def) => def.sequential === true);
8333
9237
  const group = matcherKey ? {
8334
9238
  matcher: matcherKey,
@@ -8352,7 +9256,14 @@ const QwencodeHookEntrySchema = z.looseObject({
8352
9256
  url: z.optional(z.string()),
8353
9257
  timeout: z.optional(z.number()),
8354
9258
  name: z.optional(z.string()),
8355
- description: z.optional(z.string())
9259
+ description: z.optional(z.string()),
9260
+ statusMessage: z.optional(z.string()),
9261
+ async: z.optional(z.boolean()),
9262
+ env: z.optional(z.record(z.string(), z.string())),
9263
+ shell: z.optional(z.string()),
9264
+ headers: z.optional(z.record(z.string(), z.string())),
9265
+ allowedEnvVars: z.optional(z.array(z.string())),
9266
+ once: z.optional(z.boolean())
8356
9267
  });
8357
9268
  /**
8358
9269
  * A matcher group entry in a Qwen Code event array.
@@ -8371,18 +9282,29 @@ function qwencodeMatcherEntryToCanonical(entry) {
8371
9282
  const defs = [];
8372
9283
  const hooks = entry.hooks ?? [];
8373
9284
  const sequential = entry.sequential === true;
9285
+ const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
8374
9286
  for (const h of hooks) {
8375
- const command = h.command;
8376
9287
  const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" ? h.type : "command";
9288
+ const isHttp = hookType === "http";
9289
+ const isCommand = hookType === "command";
8377
9290
  defs.push({
8378
9291
  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 }
9292
+ ...compact({
9293
+ command: h.command,
9294
+ url: h.url,
9295
+ timeout: h.timeout,
9296
+ name: h.name,
9297
+ description: h.description,
9298
+ statusMessage: h.statusMessage,
9299
+ async: isCommand ? h.async : void 0,
9300
+ env: isCommand ? h.env : void 0,
9301
+ shell: isCommand ? h.shell : void 0,
9302
+ headers: isHttp ? h.headers : void 0,
9303
+ allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
9304
+ once: isHttp ? h.once : void 0,
9305
+ sequential: sequential ? true : void 0,
9306
+ matcher
9307
+ })
8386
9308
  });
8387
9309
  }
8388
9310
  return defs;
@@ -8495,6 +9417,169 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
8495
9417
  }
8496
9418
  };
8497
9419
  //#endregion
9420
+ //#region src/features/hooks/reasonix-hooks.ts
9421
+ /**
9422
+ * Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
9423
+ * against the tool name); it is ignored on every other event.
9424
+ */
9425
+ const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
9426
+ const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
9427
+ /**
9428
+ * Convert canonical hooks config to the Reasonix `hooks` object.
9429
+ * Filters shared hooks to REASONIX_HOOK_EVENTS, merges `config.reasonix?.hooks`,
9430
+ * then maps event names and emits flat per-event hook-entry arrays.
9431
+ */
9432
+ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
9433
+ const sharedHooks = {};
9434
+ for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_REASONIX_EVENTS.has(event)) sharedHooks[event] = defs;
9435
+ const effectiveHooks = {
9436
+ ...sharedHooks,
9437
+ ...toolOverrideHooks
9438
+ };
9439
+ const result = {};
9440
+ for (const [event, defs] of Object.entries(effectiveHooks)) {
9441
+ if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
9442
+ const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
9443
+ const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
9444
+ const entries = [];
9445
+ for (const def of defs) {
9446
+ if ((def.type ?? "command") !== "command") continue;
9447
+ if (typeof def.command !== "string") continue;
9448
+ const entry = { command: def.command };
9449
+ if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
9450
+ else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
9451
+ if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
9452
+ if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
9453
+ entries.push(entry);
9454
+ }
9455
+ if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
9456
+ }
9457
+ return result;
9458
+ }
9459
+ /**
9460
+ * Reverse {@link canonicalToReasonixHooks}: parse the Reasonix `hooks` object
9461
+ * back into a canonical event -> definition[] record.
9462
+ */
9463
+ function reasonixHooksToCanonical(hooks) {
9464
+ const canonical = {};
9465
+ if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
9466
+ for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
9467
+ if (!Array.isArray(rawEntries)) continue;
9468
+ const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
9469
+ const defs = [];
9470
+ for (const rawEntry of rawEntries) {
9471
+ if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
9472
+ const entry = rawEntry;
9473
+ if (typeof entry.command !== "string") continue;
9474
+ const def = {
9475
+ type: "command",
9476
+ command: entry.command
9477
+ };
9478
+ if (typeof entry.match === "string" && entry.match !== "") def.matcher = entry.match;
9479
+ if (typeof entry.description === "string" && entry.description !== "") def.description = entry.description;
9480
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
9481
+ defs.push(def);
9482
+ }
9483
+ if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
9484
+ }
9485
+ return canonical;
9486
+ }
9487
+ /**
9488
+ * Reasonix hooks adapter.
9489
+ *
9490
+ * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
9491
+ * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
9492
+ * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
9493
+ * Only the four events documented in the upstream issue are mapped:
9494
+ * PreToolUse/PostToolUse/UserPromptSubmit/Stop (see REASONIX_HOOK_EVENTS).
9495
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
9496
+ */
9497
+ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
9498
+ constructor(params) {
9499
+ super({
9500
+ ...params,
9501
+ fileContent: params.fileContent ?? "{}"
9502
+ });
9503
+ }
9504
+ isDeletable() {
9505
+ return false;
9506
+ }
9507
+ static getSettablePaths(_options = {}) {
9508
+ return {
9509
+ relativeDirPath: REASONIX_DIR,
9510
+ relativeFilePath: REASONIX_SETTINGS_FILE_NAME
9511
+ };
9512
+ }
9513
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9514
+ const paths = ReasonixHooks.getSettablePaths({ global });
9515
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9516
+ return new ReasonixHooks({
9517
+ outputRoot,
9518
+ relativeDirPath: paths.relativeDirPath,
9519
+ relativeFilePath: paths.relativeFilePath,
9520
+ fileContent,
9521
+ validate
9522
+ });
9523
+ }
9524
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9525
+ const paths = ReasonixHooks.getSettablePaths({ global });
9526
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9527
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9528
+ let settings;
9529
+ try {
9530
+ settings = JSON.parse(existingContent);
9531
+ } catch (error) {
9532
+ throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
9533
+ }
9534
+ const config = rulesyncHooks.getJson();
9535
+ const reasonixHooks = canonicalToReasonixHooks({
9536
+ config,
9537
+ toolOverrideHooks: config.reasonix?.hooks,
9538
+ logger
9539
+ });
9540
+ const merged = {
9541
+ ...settings,
9542
+ hooks: reasonixHooks
9543
+ };
9544
+ const fileContent = JSON.stringify(merged, null, 2);
9545
+ return new ReasonixHooks({
9546
+ outputRoot,
9547
+ relativeDirPath: paths.relativeDirPath,
9548
+ relativeFilePath: paths.relativeFilePath,
9549
+ fileContent,
9550
+ validate
9551
+ });
9552
+ }
9553
+ toRulesyncHooks() {
9554
+ let settings;
9555
+ try {
9556
+ settings = JSON.parse(this.getFileContent());
9557
+ } catch (error) {
9558
+ throw new Error(`Failed to parse Reasonix hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9559
+ }
9560
+ const hooks = reasonixHooksToCanonical(settings.hooks);
9561
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9562
+ version: 1,
9563
+ hooks
9564
+ }, null, 2) });
9565
+ }
9566
+ validate() {
9567
+ return {
9568
+ success: true,
9569
+ error: null
9570
+ };
9571
+ }
9572
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9573
+ return new ReasonixHooks({
9574
+ outputRoot,
9575
+ relativeDirPath,
9576
+ relativeFilePath,
9577
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9578
+ validate: false
9579
+ });
9580
+ }
9581
+ };
9582
+ //#endregion
8498
9583
  //#region src/features/hooks/vibe-hooks.ts
8499
9584
  const VIBE_DIR = ".vibe";
8500
9585
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
@@ -8653,6 +9738,12 @@ var VibeHooks = class VibeHooks extends ToolHooks {
8653
9738
  relativeFilePath: VIBE_HOOKS_FILE_NAME
8654
9739
  };
8655
9740
  }
9741
+ static getExtraSharedWritePaths() {
9742
+ return [{
9743
+ relativeDirPath: VIBE_DIR,
9744
+ relativeFilePath: VIBE_CONFIG_FILE_NAME
9745
+ }];
9746
+ }
8656
9747
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
8657
9748
  const paths = VibeHooks.getSettablePaths({ global });
8658
9749
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({});
@@ -8720,6 +9811,19 @@ var VibeHooks = class VibeHooks extends ToolHooks {
8720
9811
  //#endregion
8721
9812
  //#region src/features/hooks/hooks-processor.ts
8722
9813
  const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
9814
+ /**
9815
+ * Event names present in the config that the target's adapter cannot emit.
9816
+ *
9817
+ * When the factory passes override-block keys through verbatim
9818
+ * (`passthroughOverrideEvents`), those keys are excluded from the check so
9819
+ * documented passthrough triggers aren't falsely reported as skipped.
9820
+ */
9821
+ function unsupportedEventNames(params) {
9822
+ const { factory, sharedHooks, effectiveHooks } = params;
9823
+ const supportedEvents = new Set(factory.supportedEvents);
9824
+ const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
9825
+ return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
9826
+ }
8723
9827
  const toolHooksFactories = /* @__PURE__ */ new Map([
8724
9828
  ["antigravity-cli", {
8725
9829
  class: AntigravityCliHooks,
@@ -8853,12 +9957,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8853
9957
  supportsGlobal: true,
8854
9958
  supportsImport: true
8855
9959
  },
8856
- supportedEvents: CLAUDE_HOOK_EVENTS,
8857
- supportedHookTypes: [
8858
- "command",
8859
- "prompt",
8860
- "http"
8861
- ],
9960
+ supportedEvents: HERMESAGENT_HOOK_EVENTS,
9961
+ supportedHookTypes: ["command"],
8862
9962
  supportsMatcher: true
8863
9963
  }],
8864
9964
  ["deepagents", {
@@ -8894,6 +9994,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8894
9994
  supportedHookTypes: ["command"],
8895
9995
  supportsMatcher: true
8896
9996
  }],
9997
+ ["kiro-ide", {
9998
+ class: KiroIdeHooks,
9999
+ meta: {
10000
+ supportsProject: true,
10001
+ supportsGlobal: true,
10002
+ supportsImport: true
10003
+ },
10004
+ supportedEvents: KIRO_IDE_HOOK_EVENTS,
10005
+ supportedHookTypes: ["command", "prompt"],
10006
+ supportsMatcher: true,
10007
+ passthroughOverrideEvents: true
10008
+ }],
8897
10009
  ["devin", {
8898
10010
  class: DevinHooks,
8899
10011
  meta: {
@@ -8902,8 +10014,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8902
10014
  supportsImport: true
8903
10015
  },
8904
10016
  supportedEvents: DEVIN_HOOK_EVENTS,
8905
- supportedHookTypes: ["command"],
8906
- supportsMatcher: false
10017
+ supportedHookTypes: ["command", "prompt"],
10018
+ supportsMatcher: true
8907
10019
  }],
8908
10020
  ["augmentcode", {
8909
10021
  class: AugmentcodeHooks,
@@ -8948,6 +10060,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8948
10060
  supportedEvents: QWENCODE_HOOK_EVENTS,
8949
10061
  supportedHookTypes: ["command"],
8950
10062
  supportsMatcher: true
10063
+ }],
10064
+ ["reasonix", {
10065
+ class: ReasonixHooks,
10066
+ meta: {
10067
+ supportsProject: true,
10068
+ supportsGlobal: true,
10069
+ supportsImport: true
10070
+ },
10071
+ supportedEvents: REASONIX_HOOK_EVENTS,
10072
+ supportedHookTypes: ["command"],
10073
+ supportsMatcher: true
8951
10074
  }]
8952
10075
  ]);
8953
10076
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -9023,8 +10146,11 @@ var HooksProcessor = class extends FeatureProcessor {
9023
10146
  ...overrideHooks
9024
10147
  };
9025
10148
  {
9026
- const supportedEvents = new Set(factory.supportedEvents);
9027
- const skipped = [...new Set(Object.keys(effectiveHooks))].filter((e) => !supportedEvents.has(e));
10149
+ const skipped = unsupportedEventNames({
10150
+ factory,
10151
+ sharedHooks,
10152
+ effectiveHooks
10153
+ });
9028
10154
  if (skipped.length > 0) this.logger.warn(`Skipped hook event(s) for ${this.toolTarget} (not supported): ${skipped.join(", ")}`);
9029
10155
  }
9030
10156
  {
@@ -9220,11 +10346,6 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
9220
10346
  }
9221
10347
  };
9222
10348
  //#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
10349
  //#region src/features/ignore/antigravity-cli-ignore.ts
9229
10350
  /**
9230
10351
  * Antigravity CLI is the successor to Gemini CLI and is built on the same
@@ -10456,9 +11577,9 @@ function parseAmpSettingsJsonc(fileContent) {
10456
11577
  }
10457
11578
  function filterMcpServers(mcpServers) {
10458
11579
  const filtered = {};
10459
- if (!isRecord$1(mcpServers)) return filtered;
11580
+ if (!isRecord(mcpServers)) return filtered;
10460
11581
  for (const [name, config] of Object.entries(mcpServers)) {
10461
- if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
11582
+ if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
10462
11583
  const filteredConfig = {};
10463
11584
  for (const [key, value] of Object.entries(config)) {
10464
11585
  if (isPrototypePollutionKey(key)) continue;
@@ -10573,7 +11694,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10573
11694
  success: true,
10574
11695
  error: null
10575
11696
  };
10576
- if (!isRecord$1(mcpServers)) return {
11697
+ if (!isRecord(mcpServers)) return {
10577
11698
  success: false,
10578
11699
  error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
10579
11700
  };
@@ -10582,7 +11703,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10582
11703
  success: false,
10583
11704
  error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
10584
11705
  };
10585
- if (!isRecord$1(serverConfig)) return {
11706
+ if (!isRecord(serverConfig)) return {
10586
11707
  success: false,
10587
11708
  error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
10588
11709
  };
@@ -11103,7 +12224,7 @@ const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
11103
12224
  function convertFromCodexFormat(codexMcp) {
11104
12225
  const result = {};
11105
12226
  for (const [name, config] of Object.entries(codexMcp)) {
11106
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
12227
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11107
12228
  const converted = {};
11108
12229
  for (const [key, value] of Object.entries(config)) {
11109
12230
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11123,7 +12244,7 @@ function convertToCodexFormat(mcpServers) {
11123
12244
  const result = {};
11124
12245
  for (const [name, config] of Object.entries(mcpServers)) {
11125
12246
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
11126
- if (!isRecord$1(config)) continue;
12247
+ if (!isRecord(config)) continue;
11127
12248
  const converted = {};
11128
12249
  for (const [key, value] of Object.entries(config)) {
11129
12250
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11185,19 +12306,19 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
11185
12306
  const strippedMcpServers = rulesyncMcp.getMcpServers();
11186
12307
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
11187
12308
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
11188
- const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
12309
+ const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
11189
12310
  return [serverName, {
11190
12311
  ...serverConfig,
11191
- ...isRecord$1(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
12312
+ ...isRecord(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
11192
12313
  }];
11193
12314
  })));
11194
12315
  const filteredMcpServers = this.removeEmptyEntries(converted);
11195
12316
  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"] : {};
12317
+ const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
11197
12318
  configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
11198
- const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
12319
+ const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
11199
12320
  const serverRecord = serverConfig;
11200
- if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
12321
+ if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
11201
12322
  ...serverRecord,
11202
12323
  tools: existingServer["tools"]
11203
12324
  }];
@@ -11707,18 +12828,20 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
11707
12828
  //#endregion
11708
12829
  //#region src/features/mcp/devin-mcp.ts
11709
12830
  /**
11710
- * MCP generator for Devin (Cascade).
12831
+ * MCP generator for Devin Local (native `.devin/` configuration).
12832
+ *
12833
+ * Devin reads MCP servers from the `mcpServers` key of its native config file:
12834
+ * - Project scope: `.devin/config.json`
12835
+ * - Global scope: `~/.config/devin/config.json`
11711
12836
  *
11712
- * Devin reads file-based MCP configuration from:
11713
- * - Project scope: `.windsurf/mcp_config.json`
11714
- * - Global scope: `~/.codeium/windsurf/mcp_config.json`
12837
+ * The config file is shared with the permissions feature (`permissions` key)
12838
+ * and, in global mode, the hooks feature (`hooks` key), so reads and writes
12839
+ * merge into the existing JSON rather than overwriting it, and the file is
12840
+ * never deleted. Each server is a stdio entry ({ command, args, env }) or a
12841
+ * remote entry ({ serverUrl | url, headers }), and may carry an optional
12842
+ * `disabledTools` array.
11715
12843
  *
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.
12844
+ * @see https://docs.devin.ai/cli/extensibility/configuration
11722
12845
  */
11723
12846
  var DevinMcp = class DevinMcp extends ToolMcp {
11724
12847
  json;
@@ -11736,14 +12859,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
11736
12859
  throw new Error(`Failed to parse Devin MCP config at ${join(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
11737
12860
  }
11738
12861
  }
12862
+ /**
12863
+ * config.json may carry the permissions/hooks features' keys, so it is never
12864
+ * deleted; only the managed `mcpServers` key is rewritten.
12865
+ */
12866
+ isDeletable() {
12867
+ return false;
12868
+ }
11739
12869
  static getSettablePaths({ global = false } = {}) {
11740
12870
  if (global) return {
11741
- relativeDirPath: CODEIUM_WINDSURF_DIR,
11742
- relativeFilePath: DEVIN_MCP_FILE_NAME
12871
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
12872
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11743
12873
  };
11744
12874
  return {
11745
- relativeDirPath: WINDSURF_DIR,
11746
- relativeFilePath: DEVIN_MCP_FILE_NAME
12875
+ relativeDirPath: DEVIN_DIR,
12876
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11747
12877
  };
11748
12878
  }
11749
12879
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
@@ -11858,7 +12988,7 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
11858
12988
  };
11859
12989
  //#endregion
11860
12990
  //#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";
12991
+ const GOOSE_PLUGIN_MCP_RELATIVE_PATH = join(GOOSE_PLUGIN_MCP_DIR_PATH, GOOSE_PLUGIN_MCP_FILE_NAME);
11862
12992
  function parseGooseConfig(fileContent, relativeDirPath, relativeFilePath) {
11863
12993
  const configPath = join(relativeDirPath, relativeFilePath);
11864
12994
  let parsed;
@@ -11945,7 +13075,7 @@ function convertServerToGooseExtension(name, config) {
11945
13075
  function convertToGooseFormat(mcpServers) {
11946
13076
  const extensions = {};
11947
13077
  for (const [name, config] of Object.entries(mcpServers)) {
11948
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13078
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11949
13079
  extensions[name] = convertServerToGooseExtension(name, config);
11950
13080
  }
11951
13081
  return extensions;
@@ -11962,7 +13092,7 @@ function convertToGooseFormat(mcpServers) {
11962
13092
  function convertFromGooseFormat(extensions) {
11963
13093
  const result = {};
11964
13094
  for (const [name, ext] of Object.entries(extensions)) {
11965
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
13095
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
11966
13096
  const server = {};
11967
13097
  const type = typeof ext.type === "string" ? ext.type : void 0;
11968
13098
  if (type === "sse") server.type = "sse";
@@ -11980,39 +13110,102 @@ function convertFromGooseFormat(extensions) {
11980
13110
  return result;
11981
13111
  }
11982
13112
  /**
13113
+ * Builds a Claude-style stdio server entry (`command`/`args`/`env`/`cwd`) for the
13114
+ * open-plugins `.mcp.json` manifest.
13115
+ */
13116
+ function buildGoosePluginStdioServer(config) {
13117
+ const server = {};
13118
+ const command = config.command;
13119
+ if (Array.isArray(command)) {
13120
+ if (typeof command[0] === "string") server.command = command[0];
13121
+ const rest = command.slice(1).filter((c) => typeof c === "string");
13122
+ const args = isStringArray(config.args) ? config.args : [];
13123
+ if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
13124
+ } else if (typeof command === "string") {
13125
+ server.command = command;
13126
+ if (isStringArray(config.args)) server.args = config.args;
13127
+ }
13128
+ if (isPlainObject(config.env)) server.env = omitPrototypePollutionKeys(config.env);
13129
+ if (typeof config.cwd === "string") server.cwd = config.cwd;
13130
+ return server;
13131
+ }
13132
+ /**
13133
+ * Converts rulesync canonical MCP servers into the Claude-style `mcpServers` map
13134
+ * used by Goose open-plugin manifests (`.agents/plugins/rulesync/.mcp.json`).
13135
+ *
13136
+ * The open-plugins manifest is **stdio-only** (no `url`/`headers`), so remote
13137
+ * (http/sse/streamable_http) and `builtin` servers cannot be represented. They
13138
+ * are skipped with a warning rather than silently dropped — remote servers stay
13139
+ * global-config-only (sync them with `--global` to `~/.config/goose/config.yaml`).
13140
+ */
13141
+ function convertToGoosePluginMcpServers(mcpServers, logger) {
13142
+ const result = {};
13143
+ for (const [name, config] of Object.entries(mcpServers)) {
13144
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
13145
+ const gooseType = resolveGooseType(config, resolveGooseUrl(config));
13146
+ if (gooseType !== "stdio") {
13147
+ 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.`);
13148
+ continue;
13149
+ }
13150
+ result[name] = buildGoosePluginStdioServer(config);
13151
+ }
13152
+ return result;
13153
+ }
13154
+ /**
11983
13155
  * Goose MCP servers.
11984
13156
  *
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.
13157
+ * Goose configures MCP servers in two locations:
13158
+ *
13159
+ * - **Global** (`--global`): "extensions" in the shared user config
13160
+ * `~/.config/goose/config.yaml`. That file also holds other Goose settings
13161
+ * (model, provider, ...), so generation merges the `extensions:` block into the
13162
+ * existing config instead of overwriting it, and the file is never deleted.
13163
+ * This location supports both stdio and remote (http/sse) servers.
13164
+ * - **Project**: a stdio-only open-plugin manifest at
13165
+ * `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
13166
+ * `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
13167
+ * Claude-style `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`
13168
+ * shape and cannot express `url`/`headers`, so remote servers are skipped with
13169
+ * a warning in project mode (use `--global` to sync them instead).
11990
13170
  *
11991
13171
  * @see https://block.github.io/goose/docs/getting-started/using-extensions/
13172
+ * @see https://github.com/block/goose/pull/9471
11992
13173
  */
11993
13174
  var GooseMcp = class GooseMcp extends ToolMcp {
11994
13175
  config;
11995
13176
  constructor(params) {
11996
13177
  super(params);
11997
- if (this.fileContent !== void 0) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
11998
- else this.config = {};
13178
+ if (this.fileContent === void 0) this.config = {};
13179
+ else if (params.global) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
13180
+ else this.config = this.fileContent ? this.parsePluginManifest(this.fileContent) : {};
13181
+ }
13182
+ parsePluginManifest(fileContent) {
13183
+ try {
13184
+ const parsed = JSON.parse(fileContent);
13185
+ return isRecord(parsed) ? parsed : {};
13186
+ } catch (error) {
13187
+ throw new Error(`Failed to parse Goose MCP manifest at ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
13188
+ }
11999
13189
  }
12000
13190
  getConfig() {
12001
13191
  return this.config;
12002
13192
  }
12003
13193
  isDeletable() {
12004
- return false;
13194
+ return !this.global;
12005
13195
  }
12006
- static getSettablePaths(_options) {
12007
- return {
13196
+ static getSettablePaths({ global = false } = {}) {
13197
+ if (global) return {
12008
13198
  relativeDirPath: GOOSE_GLOBAL_DIR,
12009
13199
  relativeFilePath: GOOSE_MCP_FILE_NAME
12010
13200
  };
13201
+ return {
13202
+ relativeDirPath: GOOSE_PLUGIN_MCP_DIR_PATH,
13203
+ relativeFilePath: GOOSE_PLUGIN_MCP_FILE_NAME
13204
+ };
12011
13205
  }
12012
13206
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12013
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
12014
13207
  const paths = this.getSettablePaths({ global });
12015
- const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
13208
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? (global ? "" : JSON.stringify({ mcpServers: {} }, null, 2));
12016
13209
  return new GooseMcp({
12017
13210
  outputRoot,
12018
13211
  relativeDirPath: paths.relativeDirPath,
@@ -12022,9 +13215,19 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12022
13215
  global
12023
13216
  });
12024
13217
  }
12025
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12026
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
13218
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
12027
13219
  const paths = this.getSettablePaths({ global });
13220
+ if (!global) {
13221
+ const mcpServers = convertToGoosePluginMcpServers(rulesyncMcp.getMcpServers(), logger);
13222
+ return new GooseMcp({
13223
+ outputRoot,
13224
+ relativeDirPath: paths.relativeDirPath,
13225
+ relativeFilePath: paths.relativeFilePath,
13226
+ fileContent: JSON.stringify({ mcpServers }, null, 2),
13227
+ validate,
13228
+ global
13229
+ });
13230
+ }
12028
13231
  const merged = {
12029
13232
  ...parseGooseConfig(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), ""), paths.relativeDirPath, paths.relativeFilePath),
12030
13233
  extensions: convertToGooseFormat(rulesyncMcp.getMcpServers())
@@ -12039,7 +13242,11 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12039
13242
  });
12040
13243
  }
12041
13244
  toRulesyncMcp() {
12042
- const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
13245
+ if (!this.global) {
13246
+ const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
13247
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
13248
+ }
13249
+ const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
12043
13250
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
12044
13251
  }
12045
13252
  validate() {
@@ -12116,7 +13323,7 @@ function convertToGrokFormat(mcpServers) {
12116
13323
  const result = {};
12117
13324
  for (const [name, config] of Object.entries(mcpServers)) {
12118
13325
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
12119
- if (!isRecord$1(config)) continue;
13326
+ if (!isRecord(config)) continue;
12120
13327
  const converted = {};
12121
13328
  for (const [key, value] of Object.entries(config)) {
12122
13329
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12131,7 +13338,7 @@ function convertToGrokFormat(mcpServers) {
12131
13338
  function convertFromGrokFormat(grokMcp) {
12132
13339
  const result = {};
12133
13340
  for (const [name, config] of Object.entries(grokMcp)) {
12134
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13341
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12135
13342
  const converted = {};
12136
13343
  for (const [key, value] of Object.entries(config)) {
12137
13344
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12294,13 +13501,13 @@ function convertServerToHermes(config) {
12294
13501
  function convertToHermesFormat(mcpServers) {
12295
13502
  const result = {};
12296
13503
  for (const [name, config] of Object.entries(mcpServers)) {
12297
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13504
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12298
13505
  result[name] = convertServerToHermes(config);
12299
13506
  }
12300
13507
  return result;
12301
13508
  }
12302
13509
  function mergeHermesMcpServers(config, mcpServers) {
12303
- const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
13510
+ const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
12304
13511
  return {
12305
13512
  ...config,
12306
13513
  mcp_servers: {
@@ -12318,7 +13525,7 @@ function mergeHermesMcpServers(config, mcpServers) {
12318
13525
  function convertFromHermesFormat(mcpServers) {
12319
13526
  const result = {};
12320
13527
  for (const [name, config] of Object.entries(mcpServers)) {
12321
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13528
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12322
13529
  const server = {};
12323
13530
  if (typeof config.command === "string") server.command = config.command;
12324
13531
  if (isStringArray(config.args)) server.args = config.args;
@@ -12354,7 +13561,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12354
13561
  return true;
12355
13562
  }
12356
13563
  setFileContent(fileContent) {
12357
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13564
+ const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12358
13565
  this.config = merged;
12359
13566
  super.setFileContent(stringifyHermesConfig(merged));
12360
13567
  }
@@ -12394,7 +13601,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12394
13601
  });
12395
13602
  }
12396
13603
  toRulesyncMcp() {
12397
- const servers = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13604
+ const servers = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12398
13605
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: servers }, null, 2) });
12399
13606
  }
12400
13607
  validate() {
@@ -13229,11 +14436,6 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
13229
14436
  }
13230
14437
  };
13231
14438
  //#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
14439
  //#region src/features/mcp/reasonix-mcp.ts
13238
14440
  const REASONIX_PLUGIN_FIELDS = [
13239
14441
  "type",
@@ -13241,13 +14443,14 @@ const REASONIX_PLUGIN_FIELDS = [
13241
14443
  "args",
13242
14444
  "env",
13243
14445
  "url",
13244
- "headers"
14446
+ "headers",
14447
+ "trusted_read_only_tools"
13245
14448
  ];
13246
14449
  var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13247
14450
  toml;
13248
14451
  constructor(params) {
13249
14452
  super(params);
13250
- this.toml = parseReasonixConfig(this.fileContent);
14453
+ this.toml = parseReasonixConfig$1(this.fileContent);
13251
14454
  }
13252
14455
  getToml() {
13253
14456
  return this.toml;
@@ -13271,7 +14474,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13271
14474
  }
13272
14475
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
13273
14476
  const paths = this.getSettablePaths({ global });
13274
- const config = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
14477
+ const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
13275
14478
  config.plugins = normalizePluginsArray(config.plugins);
13276
14479
  return new ReasonixMcp({
13277
14480
  outputRoot,
@@ -13284,7 +14487,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13284
14487
  }
13285
14488
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13286
14489
  const paths = this.getSettablePaths({ global });
13287
- const config = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
14490
+ const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
13288
14491
  config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
13289
14492
  return new ReasonixMcp({
13290
14493
  outputRoot,
@@ -13301,7 +14504,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13301
14504
  }
13302
14505
  validate() {
13303
14506
  try {
13304
- parseReasonixConfig(this.fileContent);
14507
+ parseReasonixConfig$1(this.fileContent);
13305
14508
  return {
13306
14509
  success: true,
13307
14510
  error: null
@@ -13324,7 +14527,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13324
14527
  });
13325
14528
  }
13326
14529
  };
13327
- function parseReasonixConfig(fileContent) {
14530
+ function parseReasonixConfig$1(fileContent) {
13328
14531
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
13329
14532
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
13330
14533
  return { ...parsed };
@@ -13464,17 +14667,6 @@ var RooMcp = class RooMcp extends ToolMcp {
13464
14667
  }
13465
14668
  };
13466
14669
  //#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
14670
  //#region src/features/mcp/rovodev-mcp.ts
13479
14671
  function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
13480
14672
  const configPath = join(relativeDirPath, relativeFilePath);
@@ -13568,6 +14760,178 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
13568
14760
  }
13569
14761
  };
13570
14762
  //#endregion
14763
+ //#region src/features/shared/takt-config.ts
14764
+ /**
14765
+ * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14766
+ *
14767
+ * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14768
+ * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14769
+ * instances are rejected for prototype-pollution hardening; a YAML mapping
14770
+ * always parses to a plain object.
14771
+ */
14772
+ function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14773
+ const configPath = join(relativeDirPath, relativeFilePath);
14774
+ let parsed;
14775
+ try {
14776
+ parsed = fileContent.trim() === "" ? {} : load(fileContent);
14777
+ } catch (error) {
14778
+ throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14779
+ }
14780
+ if (parsed === void 0 || parsed === null) return {};
14781
+ if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14782
+ return parsed;
14783
+ }
14784
+ //#endregion
14785
+ //#region src/features/mcp/takt-mcp.ts
14786
+ /**
14787
+ * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
14788
+ * global).
14789
+ *
14790
+ * IMPORTANT — what this adapter can and cannot represent.
14791
+ *
14792
+ * Takt does NOT have a project- or global-level registry of MCP *server
14793
+ * definitions*. The concrete `mcp_servers` map (a server's `command`/`args`/`env`
14794
+ * or `type`/`url`/`headers`) is declared per-step inside individual *workflow*
14795
+ * YAML files; there is no top-level `mcp_servers` key in `config.yaml`, and the
14796
+ * config loader hard-rejects unknown top-level keys
14797
+ * (`assertNoUnknownGlobalConfigKeys`). Writing a server map into `config.yaml`
14798
+ * would therefore both be ignored and break the user's config.
14799
+ *
14800
+ * What `config.yaml` *does* hold is the default-deny transport allowlist
14801
+ * `workflow_mcp_servers: { stdio, sse, http }`. Without it, workflow-defined MCP
14802
+ * servers are refused regardless of how they are declared. So this adapter emits
14803
+ * the transport allowlist derived from the transports present in
14804
+ * `.rulesync/mcp.json`, enabling exactly the transports the user's servers need.
14805
+ *
14806
+ * Lossiness (documented, intentional): the per-server names, commands, env, URLs
14807
+ * and headers are NOT representable in `config.yaml` and are intentionally not
14808
+ * written. Users still declare the concrete servers in their workflow YAML
14809
+ * steps; rulesync only opens the transport gate that permits them. As a
14810
+ * corollary, import (`toRulesyncMcp`) cannot reconstruct server definitions from
14811
+ * a transport allowlist and yields an empty `mcpServers` map.
14812
+ *
14813
+ * The shared `config.yaml` is merged in place: only the
14814
+ * `workflow_mcp_servers` key is set; every other top-level key (provider,
14815
+ * provider_profiles, etc.) is preserved. The file is never deleted.
14816
+ *
14817
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
14818
+ * @see https://github.com/nrslib/takt/blob/main/src/core/models/mcp-schemas.ts
14819
+ */
14820
+ var TaktMcp = class TaktMcp extends ToolMcp {
14821
+ constructor(params) {
14822
+ super({
14823
+ ...params,
14824
+ fileContent: params.fileContent ?? ""
14825
+ });
14826
+ }
14827
+ isDeletable() {
14828
+ return false;
14829
+ }
14830
+ static getSettablePaths(_options) {
14831
+ return {
14832
+ relativeDirPath: TAKT_DIR,
14833
+ relativeFilePath: TAKT_CONFIG_FILE_NAME
14834
+ };
14835
+ }
14836
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
14837
+ const paths = TaktMcp.getSettablePaths({ global });
14838
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
14839
+ return new TaktMcp({
14840
+ outputRoot,
14841
+ relativeDirPath: paths.relativeDirPath,
14842
+ relativeFilePath: paths.relativeFilePath,
14843
+ fileContent,
14844
+ validate,
14845
+ global
14846
+ });
14847
+ }
14848
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14849
+ const paths = TaktMcp.getSettablePaths({ global });
14850
+ const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
14851
+ const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14852
+ const merged = {
14853
+ ...config,
14854
+ [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14855
+ };
14856
+ return new TaktMcp({
14857
+ outputRoot,
14858
+ relativeDirPath: paths.relativeDirPath,
14859
+ relativeFilePath: paths.relativeFilePath,
14860
+ fileContent: dump(merged),
14861
+ validate,
14862
+ global
14863
+ });
14864
+ }
14865
+ /**
14866
+ * A transport allowlist cannot reconstruct the per-step server definitions,
14867
+ * so import yields an empty `mcpServers` map. This keeps the round-trip honest
14868
+ * rather than fabricating placeholder servers.
14869
+ */
14870
+ toRulesyncMcp() {
14871
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: {} }, null, 2) });
14872
+ }
14873
+ validate() {
14874
+ return {
14875
+ success: true,
14876
+ error: null
14877
+ };
14878
+ }
14879
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
14880
+ return new TaktMcp({
14881
+ outputRoot,
14882
+ relativeDirPath,
14883
+ relativeFilePath,
14884
+ fileContent: "",
14885
+ validate: false,
14886
+ global
14887
+ });
14888
+ }
14889
+ };
14890
+ /**
14891
+ * Map a rulesync MCP server onto the single Takt transport its allowlist gates.
14892
+ *
14893
+ * Takt allows only `stdio` / `sse` / `http`, so the broader rulesync alias set is
14894
+ * folded: `local` ⇒ stdio; `streamable-http` / `ws` ⇒ http. A server with no
14895
+ * explicit transport is treated as stdio when it carries a `command`, else as a
14896
+ * remote `http` server (it must have a `url`). Returns `undefined` only when the
14897
+ * shape is too ambiguous to classify.
14898
+ */
14899
+ function transportOf(server) {
14900
+ switch (server.type ?? server.transport) {
14901
+ case "stdio":
14902
+ case "local": return "stdio";
14903
+ case "sse": return "sse";
14904
+ case "http":
14905
+ case "streamable-http":
14906
+ case "ws": return "http";
14907
+ default: break;
14908
+ }
14909
+ if (server.command !== void 0) return "stdio";
14910
+ if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
14911
+ }
14912
+ /**
14913
+ * Derive Takt's `workflow_mcp_servers` allowlist from the transports present in
14914
+ * the rulesync servers. All three keys are emitted explicitly (default-deny made
14915
+ * visible): a transport is `true` only when at least one server uses it.
14916
+ *
14917
+ * Server entries are read defensively (record guard); prototype-pollution server
14918
+ * names are irrelevant here because no user-controlled key or value is written —
14919
+ * only the three fixed boolean keys are.
14920
+ */
14921
+ function deriveTransportAllowlist(servers) {
14922
+ const allowlist = {
14923
+ stdio: false,
14924
+ sse: false,
14925
+ http: false
14926
+ };
14927
+ for (const server of Object.values(servers)) {
14928
+ if (!isRecord(server)) continue;
14929
+ const transport = transportOf(server);
14930
+ if (transport) allowlist[transport] = true;
14931
+ }
14932
+ return allowlist;
14933
+ }
14934
+ //#endregion
13571
14935
  //#region src/features/mcp/vibe-mcp.ts
13572
14936
  const VIBE_MCP_SERVER_FIELDS = [
13573
14937
  "transport",
@@ -14028,7 +15392,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14028
15392
  ["goose", {
14029
15393
  class: GooseMcp,
14030
15394
  meta: {
14031
- supportsProject: false,
15395
+ supportsProject: true,
14032
15396
  supportsGlobal: true,
14033
15397
  supportsEnabledTools: false,
14034
15398
  supportsDisabledTools: false
@@ -14142,6 +15506,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14142
15506
  supportsDisabledTools: false
14143
15507
  }
14144
15508
  }],
15509
+ ["takt", {
15510
+ class: TaktMcp,
15511
+ meta: {
15512
+ supportsProject: true,
15513
+ supportsGlobal: true,
15514
+ supportsEnabledTools: false,
15515
+ supportsDisabledTools: false
15516
+ }
15517
+ }],
14145
15518
  ["vibe", {
14146
15519
  class: VibeMcp,
14147
15520
  meta: {
@@ -14270,7 +15643,8 @@ var McpProcessor = class extends FeatureProcessor {
14270
15643
  return await factory.class.fromRulesyncMcp({
14271
15644
  outputRoot: this.outputRoot,
14272
15645
  rulesyncMcp: filteredRulesyncMcp,
14273
- global: this.global
15646
+ global: this.global,
15647
+ logger: this.logger
14274
15648
  });
14275
15649
  }));
14276
15650
  }
@@ -14784,7 +16158,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
14784
16158
  function toAntigravityCliToolName(canonical) {
14785
16159
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
14786
16160
  }
14787
- function toCanonicalToolName$5(cliName) {
16161
+ function toCanonicalToolName$6(cliName) {
14788
16162
  return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
14789
16163
  }
14790
16164
  /**
@@ -14967,7 +16341,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
14967
16341
  const processEntries = (entries, action) => {
14968
16342
  for (const entry of entries) {
14969
16343
  const { toolName, pattern } = parsePermissionEntry$1(entry);
14970
- const canonical = toCanonicalToolName$5(toolName);
16344
+ const canonical = toCanonicalToolName$6(toolName);
14971
16345
  if (!permission[canonical]) permission[canonical] = {};
14972
16346
  permission[canonical][pattern] = action;
14973
16347
  }
@@ -15006,7 +16380,7 @@ const IDE_ACTION_TO_CANONICAL = {
15006
16380
  function toIdeAction(canonical) {
15007
16381
  return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
15008
16382
  }
15009
- function toCanonicalCategory$1(ideAction) {
16383
+ function toCanonicalCategory$2(ideAction) {
15010
16384
  return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
15011
16385
  }
15012
16386
  /**
@@ -15180,7 +16554,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
15180
16554
  const processEntries = (entries, action) => {
15181
16555
  for (const entry of entries) {
15182
16556
  const { action: ideAction, pattern } = parsePermissionEntry(entry);
15183
- const canonical = toCanonicalCategory$1(ideAction);
16557
+ const canonical = toCanonicalCategory$2(ideAction);
15184
16558
  if (!permission[canonical]) permission[canonical] = {};
15185
16559
  permission[canonical][pattern] = action;
15186
16560
  }
@@ -15229,7 +16603,7 @@ const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONI
15229
16603
  function toAugmentToolName(canonical) {
15230
16604
  return CANONICAL_TO_AUGMENT_TOOL_NAMES[canonical] ?? canonical;
15231
16605
  }
15232
- function toCanonicalToolName$4(augmentName) {
16606
+ function toCanonicalToolName$5(augmentName) {
15233
16607
  return AUGMENT_TO_CANONICAL_TOOL_NAMES[augmentName] ?? augmentName;
15234
16608
  }
15235
16609
  function actionToAugmentType(action) {
@@ -15573,7 +16947,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
15573
16947
  }
15574
16948
  const type = entry.permission.type;
15575
16949
  if (!isBasicAugmentType(type)) continue;
15576
- const canonical = toCanonicalToolName$4(entry.toolName);
16950
+ const canonical = toCanonicalToolName$5(entry.toolName);
15577
16951
  if (forbiddenMapKeys.has(canonical)) {
15578
16952
  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
16953
  continue;
@@ -15626,7 +17000,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
15626
17000
  function toClaudeToolName(canonical) {
15627
17001
  return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
15628
17002
  }
15629
- function toCanonicalToolName$3(claudeName) {
17003
+ function toCanonicalToolName$4(claudeName) {
15630
17004
  return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
15631
17005
  }
15632
17006
  /**
@@ -15800,7 +17174,7 @@ function convertClaudeToRulesyncPermissions(params) {
15800
17174
  const processEntries = (entries, action) => {
15801
17175
  for (const entry of entries) {
15802
17176
  const { toolName, pattern } = parseClaudePermissionEntry(entry);
15803
- const canonical = toCanonicalToolName$3(toolName);
17177
+ const canonical = toCanonicalToolName$4(toolName);
15804
17178
  if (!permission[canonical]) permission[canonical] = {};
15805
17179
  permission[canonical][pattern] = action;
15806
17180
  }
@@ -16427,7 +17801,7 @@ function toCursorPattern(canonical, pattern) {
16427
17801
  }
16428
17802
  return pattern;
16429
17803
  }
16430
- function toCanonicalCategory(cursorType, pattern) {
17804
+ function toCanonicalCategory$1(cursorType, pattern) {
16431
17805
  if (cursorType === "Mcp") {
16432
17806
  const match = pattern.match(/^([^:()]+):([^:()]+)$/);
16433
17807
  if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
@@ -16652,7 +18026,7 @@ function convertCursorToRulesyncPermissions(params) {
16652
18026
  const processEntries = (entries, action) => {
16653
18027
  for (const entry of entries) {
16654
18028
  const { type, pattern } = parseCursorPermissionEntry(entry);
16655
- const canonical = toCanonicalCategory(type, pattern);
18029
+ const canonical = toCanonicalCategory$1(type, pattern);
16656
18030
  if (!permission[canonical]) permission[canonical] = {};
16657
18031
  const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
16658
18032
  permission[canonical][canonicalPattern] = action;
@@ -16663,6 +18037,245 @@ function convertCursorToRulesyncPermissions(params) {
16663
18037
  return { permission };
16664
18038
  }
16665
18039
  //#endregion
18040
+ //#region src/features/permissions/devin-permissions.ts
18041
+ /**
18042
+ * Mapping from rulesync canonical tool category names to Devin Local permission
18043
+ * scope matchers.
18044
+ *
18045
+ * Devin expresses permissions with scope-based matchers — `Read(glob)`,
18046
+ * `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
18047
+ * (`mcp__server__tool`). The canonical `edit` and `write` categories both map
18048
+ * onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
18049
+ * `edit` rules round-trip as `write` (a lossy but documented collapse). Unknown
18050
+ * names (e.g. `mcp__github__list_issues`) pass through verbatim.
18051
+ *
18052
+ * @see https://docs.devin.ai/cli/reference/permissions
18053
+ */
18054
+ const CANONICAL_TO_DEVIN_SCOPE = {
18055
+ read: "Read",
18056
+ write: "Write",
18057
+ edit: "Write",
18058
+ bash: "Exec",
18059
+ webfetch: "Fetch"
18060
+ };
18061
+ /**
18062
+ * Reverse mapping from Devin scope matchers to rulesync canonical names.
18063
+ */
18064
+ const DEVIN_SCOPE_TO_CANONICAL = {
18065
+ Read: "read",
18066
+ Write: "write",
18067
+ Exec: "bash",
18068
+ Fetch: "webfetch"
18069
+ };
18070
+ function toDevinScope(canonical) {
18071
+ return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
18072
+ }
18073
+ function toCanonicalCategory(devinScope) {
18074
+ return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
18075
+ }
18076
+ /**
18077
+ * Parse a Devin permission entry like `Read(src/**)` into scope and pattern.
18078
+ * Bare entries (e.g. `Read`, or a whole-tool name like `exec`) yield `*`.
18079
+ */
18080
+ function parseDevinPermissionEntry(entry) {
18081
+ const parenIndex = entry.indexOf("(");
18082
+ if (parenIndex === -1) return {
18083
+ scope: entry,
18084
+ pattern: "*"
18085
+ };
18086
+ const scope = entry.slice(0, parenIndex);
18087
+ if (!entry.endsWith(")")) return {
18088
+ scope,
18089
+ pattern: "*"
18090
+ };
18091
+ return {
18092
+ scope,
18093
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
18094
+ };
18095
+ }
18096
+ /**
18097
+ * Build a Devin permission entry like `Read(src/**)`. A `*` pattern collapses to
18098
+ * the bare scope (`Read`), matching the whole scope.
18099
+ */
18100
+ function buildDevinPermissionEntry(scope, pattern) {
18101
+ if (pattern === "*") return scope;
18102
+ return `${scope}(${pattern})`;
18103
+ }
18104
+ /**
18105
+ * Permissions generator for Devin Local (native `.devin/` configuration).
18106
+ *
18107
+ * Maps rulesync permission actions onto Devin's `permissions` block inside its
18108
+ * native config file — `allow` / `deny` / `ask` arrays of scope matchers
18109
+ * (`Read(glob)`, `Write(glob)`, `Exec(prefix)`, `Fetch(pattern)`, plus
18110
+ * `mcp__server__tool` patterns). Devin evaluates the arrays with strict
18111
+ * precedence: `deny` is checked before `ask`, which is checked before `allow`,
18112
+ * so a deny rule always wins.
18113
+ *
18114
+ * - Project scope: `.devin/config.json`
18115
+ * - Global scope: `~/.config/devin/config.json`
18116
+ *
18117
+ * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
18118
+ * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
18119
+ * the file is never deleted; only the managed `permissions` key is rewritten.
18120
+ *
18121
+ * @see https://docs.devin.ai/cli/reference/permissions
18122
+ */
18123
+ var DevinPermissions = class DevinPermissions extends ToolPermissions {
18124
+ constructor(params) {
18125
+ super({
18126
+ ...params,
18127
+ fileContent: params.fileContent ?? "{}"
18128
+ });
18129
+ }
18130
+ /**
18131
+ * config.json may carry the MCP/hooks features' keys, so it is never deleted;
18132
+ * only the managed `permissions` key is rewritten.
18133
+ */
18134
+ isDeletable() {
18135
+ return false;
18136
+ }
18137
+ static getSettablePaths({ global = false } = {}) {
18138
+ if (global) return {
18139
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
18140
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18141
+ };
18142
+ return {
18143
+ relativeDirPath: DEVIN_DIR,
18144
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18145
+ };
18146
+ }
18147
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18148
+ const paths = DevinPermissions.getSettablePaths({ global });
18149
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"permissions\":{}}";
18150
+ return new DevinPermissions({
18151
+ outputRoot,
18152
+ relativeDirPath: paths.relativeDirPath,
18153
+ relativeFilePath: paths.relativeFilePath,
18154
+ fileContent,
18155
+ validate
18156
+ });
18157
+ }
18158
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
18159
+ const paths = DevinPermissions.getSettablePaths({ global });
18160
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18161
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
18162
+ let settings;
18163
+ try {
18164
+ const parsed = JSON.parse(existingContent);
18165
+ settings = isRecord(parsed) ? parsed : {};
18166
+ } catch (error) {
18167
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
18168
+ }
18169
+ const config = rulesyncPermissions.getJson();
18170
+ const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
18171
+ const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
18172
+ const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
18173
+ const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
18174
+ const mergedAllow = uniq([...preserve(existingPermissions.allow), ...allow].toSorted());
18175
+ const mergedAsk = uniq([...preserve(existingPermissions.ask), ...ask].toSorted());
18176
+ const mergedDeny = uniq([...preserve(existingPermissions.deny), ...deny].toSorted());
18177
+ const mergedPermissions = { ...existingPermissions };
18178
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
18179
+ else delete mergedPermissions.allow;
18180
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
18181
+ else delete mergedPermissions.ask;
18182
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
18183
+ else delete mergedPermissions.deny;
18184
+ const merged = {
18185
+ ...settings,
18186
+ permissions: mergedPermissions
18187
+ };
18188
+ return new DevinPermissions({
18189
+ outputRoot,
18190
+ relativeDirPath: paths.relativeDirPath,
18191
+ relativeFilePath: paths.relativeFilePath,
18192
+ fileContent: JSON.stringify(merged, null, 2),
18193
+ validate
18194
+ });
18195
+ }
18196
+ toRulesyncPermissions() {
18197
+ let settings;
18198
+ try {
18199
+ const parsed = JSON.parse(this.getFileContent());
18200
+ settings = isRecord(parsed) ? parsed : {};
18201
+ } catch (error) {
18202
+ throw new Error(`Failed to parse Devin permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18203
+ }
18204
+ const permissions = isRecord(settings.permissions) ? settings.permissions : {};
18205
+ const config = convertDevinToRulesyncPermissions({
18206
+ allow: Array.isArray(permissions.allow) ? permissions.allow : [],
18207
+ ask: Array.isArray(permissions.ask) ? permissions.ask : [],
18208
+ deny: Array.isArray(permissions.deny) ? permissions.deny : []
18209
+ });
18210
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18211
+ }
18212
+ validate() {
18213
+ return {
18214
+ success: true,
18215
+ error: null
18216
+ };
18217
+ }
18218
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18219
+ return new DevinPermissions({
18220
+ outputRoot,
18221
+ relativeDirPath,
18222
+ relativeFilePath,
18223
+ fileContent: JSON.stringify({ permissions: {} }, null, 2),
18224
+ validate: false
18225
+ });
18226
+ }
18227
+ };
18228
+ /**
18229
+ * Convert rulesync permissions config to Devin allow/ask/deny arrays.
18230
+ */
18231
+ function convertRulesyncToDevinPermissions(config) {
18232
+ const allow = [];
18233
+ const ask = [];
18234
+ const deny = [];
18235
+ for (const [category, rules] of Object.entries(config.permission)) {
18236
+ const scope = toDevinScope(category);
18237
+ for (const [pattern, action] of Object.entries(rules)) {
18238
+ const entry = buildDevinPermissionEntry(scope, pattern);
18239
+ switch (action) {
18240
+ case "allow":
18241
+ allow.push(entry);
18242
+ break;
18243
+ case "ask":
18244
+ ask.push(entry);
18245
+ break;
18246
+ case "deny":
18247
+ deny.push(entry);
18248
+ break;
18249
+ }
18250
+ }
18251
+ }
18252
+ return {
18253
+ allow,
18254
+ ask,
18255
+ deny
18256
+ };
18257
+ }
18258
+ /**
18259
+ * Convert Devin allow/ask/deny arrays to rulesync permissions config. Entries
18260
+ * are applied allow → ask → deny so the most restrictive action wins for a
18261
+ * given (scope, pattern), mirroring Devin's deny > ask > allow precedence.
18262
+ */
18263
+ function convertDevinToRulesyncPermissions(params) {
18264
+ const permission = {};
18265
+ const processEntries = (entries, action) => {
18266
+ for (const entry of entries) {
18267
+ const { scope, pattern } = parseDevinPermissionEntry(entry);
18268
+ if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
18269
+ const canonical = toCanonicalCategory(scope);
18270
+ (permission[canonical] ??= {})[pattern] = action;
18271
+ }
18272
+ };
18273
+ processEntries(params.allow, "allow");
18274
+ processEntries(params.ask, "ask");
18275
+ processEntries(params.deny, "deny");
18276
+ return { permission };
18277
+ }
18278
+ //#endregion
16666
18279
  //#region src/features/permissions/factorydroid-permissions.ts
16667
18280
  /**
16668
18281
  * Permissions adapter for Factory Droid.
@@ -16922,7 +18535,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16922
18535
  } catch (error) {
16923
18536
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
16924
18537
  }
16925
- const config = isRecord$1(parsed) ? { ...parsed } : {};
18538
+ const config = isRecord(parsed) ? { ...parsed } : {};
16926
18539
  config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
16927
18540
  config: rulesyncPermissions.getJson(),
16928
18541
  logger
@@ -16944,8 +18557,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16944
18557
  } catch (error) {
16945
18558
  throw new Error(`Failed to parse Goose permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
16946
18559
  }
16947
- const config = isRecord$1(parsed) ? parsed : {};
16948
- const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
18560
+ const config = isRecord(parsed) ? parsed : {};
18561
+ const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
16949
18562
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
16950
18563
  }
16951
18564
  validate() {
@@ -17085,7 +18698,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17085
18698
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
17086
18699
  }
17087
18700
  const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
17088
- const existingUi = isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
18701
+ const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
17089
18702
  parsed[GROKCLI_UI_KEY] = {
17090
18703
  ...existingUi,
17091
18704
  [GROKCLI_PERMISSION_MODE_KEY]: mode
@@ -17107,7 +18720,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17107
18720
  } catch (error) {
17108
18721
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
17109
18722
  }
17110
- const action = (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
18723
+ const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
17111
18724
  const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
17112
18725
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
17113
18726
  }
@@ -17189,6 +18802,205 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
17189
18802
  }
17190
18803
  };
17191
18804
  //#endregion
18805
+ //#region src/features/permissions/junie-permissions.ts
18806
+ /**
18807
+ * JetBrains Junie CLI Action Allowlist (`allowlist.json`).
18808
+ *
18809
+ * Junie gates actions through an allowlist evaluated top-to-bottom (first match
18810
+ * wins). Project scope lives in `.junie/allowlist.json`; user scope lives in
18811
+ * `~/.junie/allowlist.json`.
18812
+ *
18813
+ * ```json
18814
+ * {
18815
+ * "defaultBehavior": "ask",
18816
+ * "allowReadonlyCommands": true,
18817
+ * "rules": {
18818
+ * "executables": [ { "prefix": "git ", "action": "allow" } ],
18819
+ * "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
18820
+ * "mcpTools": [ { "prefix": "search", "action": "allow" } ],
18821
+ * "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
18822
+ * }
18823
+ * }
18824
+ * ```
18825
+ *
18826
+ * Each rule carries a literal `prefix` (matches commands that start with it) or
18827
+ * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
18828
+ * (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
18829
+ *
18830
+ * Category mapping (rulesync canonical <-> Junie rule group):
18831
+ * - `bash` <-> `executables`
18832
+ * - `edit`/`write` -> `fileEditing` (imported back as `edit`)
18833
+ * - `read` <-> `readOutsideProject`
18834
+ * - `mcp` <-> `mcpTools`
18835
+ *
18836
+ * Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
18837
+ * (with a warning when they carry rules). The top-level `defaultBehavior` and
18838
+ * `allowReadonlyCommands` settings have no canonical equivalent: they are
18839
+ * preserved verbatim on export but not imported into the rulesync model.
18840
+ *
18841
+ * @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
18842
+ */
18843
+ const JUNIE_RULE_GROUPS = [
18844
+ "executables",
18845
+ "fileEditing",
18846
+ "mcpTools",
18847
+ "readOutsideProject"
18848
+ ];
18849
+ const CANONICAL_TO_JUNIE_GROUP = {
18850
+ bash: "executables",
18851
+ edit: "fileEditing",
18852
+ write: "fileEditing",
18853
+ read: "readOutsideProject",
18854
+ mcp: "mcpTools"
18855
+ };
18856
+ const JUNIE_GROUP_TO_CANONICAL = {
18857
+ executables: "bash",
18858
+ fileEditing: "edit",
18859
+ mcpTools: "mcp",
18860
+ readOutsideProject: "read"
18861
+ };
18862
+ const JUNIE_DEFAULT_BEHAVIOR = "ask";
18863
+ function isPermissionAction$1(value) {
18864
+ return PermissionActionSchema.safeParse(value).success;
18865
+ }
18866
+ /**
18867
+ * Whether a rulesync pattern uses glob syntax. Junie expresses literal
18868
+ * "starts-with" matches as `prefix` and glob matches as `pattern`, so a pattern
18869
+ * containing any glob metacharacter (`*`, `?`, `[`) is emitted as `pattern`.
18870
+ */
18871
+ function isGlobPattern(pattern) {
18872
+ return /[*?[]/.test(pattern);
18873
+ }
18874
+ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18875
+ constructor(params) {
18876
+ super({
18877
+ ...params,
18878
+ fileContent: params.fileContent ?? "{}"
18879
+ });
18880
+ }
18881
+ isDeletable() {
18882
+ return false;
18883
+ }
18884
+ static getSettablePaths(_options = {}) {
18885
+ return {
18886
+ relativeDirPath: JUNIE_DIR,
18887
+ relativeFilePath: JUNIE_PERMISSIONS_FILE_NAME
18888
+ };
18889
+ }
18890
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18891
+ const paths = JuniePermissions.getSettablePaths({ global });
18892
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
18893
+ return new JuniePermissions({
18894
+ outputRoot,
18895
+ relativeDirPath: paths.relativeDirPath,
18896
+ relativeFilePath: paths.relativeFilePath,
18897
+ fileContent,
18898
+ validate
18899
+ });
18900
+ }
18901
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
18902
+ const paths = JuniePermissions.getSettablePaths({ global });
18903
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18904
+ const existingContent = await readOrInitializeFileContent(filePath, "{}");
18905
+ let existing;
18906
+ try {
18907
+ const parsed = JSON.parse(existingContent);
18908
+ existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18909
+ } catch (error) {
18910
+ throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
18911
+ }
18912
+ const rules = convertRulesyncToJunieRules({
18913
+ config: rulesyncPermissions.getJson(),
18914
+ logger
18915
+ });
18916
+ const merged = {
18917
+ ...existing,
18918
+ defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
18919
+ rules
18920
+ };
18921
+ return new JuniePermissions({
18922
+ outputRoot,
18923
+ relativeDirPath: paths.relativeDirPath,
18924
+ relativeFilePath: paths.relativeFilePath,
18925
+ fileContent: JSON.stringify(merged, null, 2),
18926
+ validate: true
18927
+ });
18928
+ }
18929
+ toRulesyncPermissions() {
18930
+ let allowlist;
18931
+ try {
18932
+ const parsed = JSON.parse(this.getFileContent());
18933
+ allowlist = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18934
+ } catch (error) {
18935
+ throw new Error(`Failed to parse Junie permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18936
+ }
18937
+ const config = convertJunieToRulesyncPermissions({ allowlist });
18938
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18939
+ }
18940
+ validate() {
18941
+ return {
18942
+ success: true,
18943
+ error: null
18944
+ };
18945
+ }
18946
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18947
+ return new JuniePermissions({
18948
+ outputRoot,
18949
+ relativeDirPath,
18950
+ relativeFilePath,
18951
+ fileContent: "{}",
18952
+ validate: false
18953
+ });
18954
+ }
18955
+ };
18956
+ /**
18957
+ * Convert rulesync permissions config into Junie's `rules` object. Categories
18958
+ * with no Junie rule group (e.g. `webfetch`) are skipped, with a warning when
18959
+ * they carry any rule so the gap is surfaced.
18960
+ */
18961
+ function convertRulesyncToJunieRules({ config, logger }) {
18962
+ const rules = {};
18963
+ for (const [category, patterns] of Object.entries(config.permission)) {
18964
+ const group = CANONICAL_TO_JUNIE_GROUP[category];
18965
+ if (!group) {
18966
+ 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.`);
18967
+ continue;
18968
+ }
18969
+ for (const [pattern, action] of Object.entries(patterns)) {
18970
+ const rule = isGlobPattern(pattern) ? {
18971
+ pattern,
18972
+ action
18973
+ } : {
18974
+ prefix: pattern,
18975
+ action
18976
+ };
18977
+ (rules[group] ??= []).push(rule);
18978
+ }
18979
+ }
18980
+ return rules;
18981
+ }
18982
+ /**
18983
+ * Convert a Junie allowlist back into rulesync permissions config. The
18984
+ * top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
18985
+ * canonical equivalent and are not imported.
18986
+ */
18987
+ function convertJunieToRulesyncPermissions({ allowlist }) {
18988
+ const permission = {};
18989
+ const rules = allowlist.rules;
18990
+ if (rules && typeof rules === "object") for (const group of JUNIE_RULE_GROUPS) {
18991
+ const list = rules[group];
18992
+ if (!Array.isArray(list)) continue;
18993
+ const category = JUNIE_GROUP_TO_CANONICAL[group];
18994
+ for (const rule of list) {
18995
+ if (!rule || typeof rule !== "object") continue;
18996
+ const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
18997
+ if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
18998
+ (permission[category] ??= {})[pattern] = rule.action;
18999
+ }
19000
+ }
19001
+ return { permission };
19002
+ }
19003
+ //#endregion
17192
19004
  //#region src/features/permissions/kilo-permissions.ts
17193
19005
  const KiloPermissionSchema = z.union([z.enum([
17194
19006
  "allow",
@@ -17537,7 +19349,11 @@ const OpencodePermissionSchema = z.union([z.enum([
17537
19349
  "ask",
17538
19350
  "deny"
17539
19351
  ]))]);
17540
- const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodePermissionSchema)) });
19352
+ const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.union([z.enum([
19353
+ "allow",
19354
+ "ask",
19355
+ "deny"
19356
+ ]), z.record(z.string(), OpencodePermissionSchema)])) });
17541
19357
  var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17542
19358
  json;
17543
19359
  constructor(params) {
@@ -17640,6 +19456,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17640
19456
  }
17641
19457
  normalizePermission(permission) {
17642
19458
  if (!permission) return {};
19459
+ if (typeof permission === "string") return { "*": { "*": permission } };
17643
19460
  return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
17644
19461
  }
17645
19462
  };
@@ -17676,7 +19493,7 @@ const QWEN_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL
17676
19493
  function toQwenToolName(canonical) {
17677
19494
  return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
17678
19495
  }
17679
- function toCanonicalToolName$2(qwenName) {
19496
+ function toCanonicalToolName$3(qwenName) {
17680
19497
  return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
17681
19498
  }
17682
19499
  function parseQwenPermissionEntry(entry, options = {}) {
@@ -17870,13 +19687,242 @@ function convertQwenToRulesyncPermissions(params) {
17870
19687
  const parsed = parseQwenPermissionEntry(entry, { logger });
17871
19688
  if (!parsed.ok) {
17872
19689
  if (action === "deny") {
17873
- const canonical = toCanonicalToolName$2(parsed.toolName);
19690
+ const canonical = toCanonicalToolName$3(parsed.toolName);
17874
19691
  if (!permission[canonical]) permission[canonical] = {};
17875
19692
  permission[canonical]["*"] = action;
17876
19693
  }
17877
19694
  continue;
17878
19695
  }
17879
19696
  const { toolName, pattern } = parsed;
19697
+ const canonical = toCanonicalToolName$3(toolName);
19698
+ if (!permission[canonical]) permission[canonical] = {};
19699
+ permission[canonical][pattern] = action;
19700
+ }
19701
+ };
19702
+ processEntries(params.allow, "allow");
19703
+ processEntries(params.ask, "ask");
19704
+ processEntries(params.deny, "deny");
19705
+ return { permission };
19706
+ }
19707
+ //#endregion
19708
+ //#region src/features/permissions/reasonix-permissions.ts
19709
+ /**
19710
+ * Mapping from rulesync canonical tool category names (lowercase) to Reasonix
19711
+ * permission-rule tool families (PascalCase).
19712
+ *
19713
+ * Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
19714
+ * documented as "Claude Code-style": "Bash and file mutation approvals use
19715
+ * Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
19716
+ * test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
19717
+ * IDs for compatibility, but new rules are saved using these PascalCase
19718
+ * families, so rulesync reuses the same mapping `claudecode-permissions.ts`
19719
+ * uses (the closest documented precedent for this syntax).
19720
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
19721
+ */
19722
+ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
19723
+ bash: "Bash",
19724
+ read: "Read",
19725
+ edit: "Edit",
19726
+ write: "Write",
19727
+ webfetch: "WebFetch",
19728
+ websearch: "WebSearch",
19729
+ grep: "Grep",
19730
+ glob: "Glob",
19731
+ notebookedit: "NotebookEdit",
19732
+ agent: "Agent"
19733
+ };
19734
+ /**
19735
+ * Reverse mapping from Reasonix tool names to rulesync canonical names.
19736
+ */
19737
+ const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
19738
+ function toReasonixToolName(canonical) {
19739
+ return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
19740
+ }
19741
+ function toCanonicalToolName$2(reasonixName) {
19742
+ return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
19743
+ }
19744
+ /**
19745
+ * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
19746
+ * If no parentheses, returns the tool name with "*" as the pattern.
19747
+ */
19748
+ function parseReasonixPermissionEntry(entry) {
19749
+ const parenIndex = entry.indexOf("(");
19750
+ if (parenIndex === -1) return {
19751
+ toolName: entry,
19752
+ pattern: "*"
19753
+ };
19754
+ const toolName = entry.slice(0, parenIndex);
19755
+ if (!entry.endsWith(")")) return {
19756
+ toolName,
19757
+ pattern: "*"
19758
+ };
19759
+ return {
19760
+ toolName,
19761
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
19762
+ };
19763
+ }
19764
+ /**
19765
+ * Build a Reasonix permission entry like "Bash(npm run *)".
19766
+ * If the pattern is "*", returns just the tool name.
19767
+ */
19768
+ function buildReasonixPermissionEntry(toolName, pattern) {
19769
+ if (pattern === "*") return toolName;
19770
+ return `${toolName}(${pattern})`;
19771
+ }
19772
+ function parseReasonixConfig(fileContent) {
19773
+ const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
19774
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
19775
+ return { ...parsed };
19776
+ }
19777
+ function toStringArray$1(value) {
19778
+ if (!Array.isArray(value)) return [];
19779
+ return value.filter((entry) => typeof entry === "string");
19780
+ }
19781
+ function toPermissionsTable(value) {
19782
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
19783
+ return { ...value };
19784
+ }
19785
+ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19786
+ toml;
19787
+ constructor(params) {
19788
+ super(params);
19789
+ this.toml = parseReasonixConfig(this.getFileContent());
19790
+ }
19791
+ isDeletable() {
19792
+ return false;
19793
+ }
19794
+ static getSettablePaths({ global } = {}) {
19795
+ if (global) return {
19796
+ relativeDirPath: REASONIX_GLOBAL_DIR,
19797
+ relativeFilePath: REASONIX_GLOBAL_PERMISSIONS_FILE_NAME
19798
+ };
19799
+ return {
19800
+ relativeDirPath: ".",
19801
+ relativeFilePath: REASONIX_PROJECT_PERMISSIONS_FILE_NAME
19802
+ };
19803
+ }
19804
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
19805
+ const paths = this.getSettablePaths({ global });
19806
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({});
19807
+ return new ReasonixPermissions({
19808
+ outputRoot,
19809
+ relativeDirPath: paths.relativeDirPath,
19810
+ relativeFilePath: paths.relativeFilePath,
19811
+ fileContent,
19812
+ validate
19813
+ });
19814
+ }
19815
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
19816
+ const paths = this.getSettablePaths({ global });
19817
+ const parsed = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
19818
+ const config = rulesyncPermissions.getJson();
19819
+ const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
19820
+ const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
19821
+ const existingPermissions = toPermissionsTable(parsed.permissions);
19822
+ const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19823
+ const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19824
+ const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19825
+ if (logger && managedToolNames.has("Read")) {
19826
+ const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
19827
+ const { toolName } = parseReasonixPermissionEntry(entry);
19828
+ return toolName === "Read";
19829
+ });
19830
+ 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.`);
19831
+ }
19832
+ const mergedPermissions = { ...existingPermissions };
19833
+ const mergedAllow = uniq([...preservedAllow, ...allow].toSorted());
19834
+ const mergedAsk = uniq([...preservedAsk, ...ask].toSorted());
19835
+ const mergedDeny = uniq([...preservedDeny, ...deny].toSorted());
19836
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
19837
+ else delete mergedPermissions.allow;
19838
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
19839
+ else delete mergedPermissions.ask;
19840
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
19841
+ else delete mergedPermissions.deny;
19842
+ const merged = {
19843
+ ...parsed,
19844
+ permissions: mergedPermissions
19845
+ };
19846
+ const fileContent = smolToml.stringify(merged);
19847
+ return new ReasonixPermissions({
19848
+ outputRoot,
19849
+ relativeDirPath: paths.relativeDirPath,
19850
+ relativeFilePath: paths.relativeFilePath,
19851
+ fileContent,
19852
+ validate
19853
+ });
19854
+ }
19855
+ toRulesyncPermissions() {
19856
+ const permissions = toPermissionsTable(this.toml.permissions);
19857
+ const config = convertReasonixToRulesyncPermissions({
19858
+ allow: toStringArray$1(permissions.allow),
19859
+ ask: toStringArray$1(permissions.ask),
19860
+ deny: toStringArray$1(permissions.deny)
19861
+ });
19862
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19863
+ }
19864
+ validate() {
19865
+ try {
19866
+ parseReasonixConfig(this.getFileContent());
19867
+ return {
19868
+ success: true,
19869
+ error: null
19870
+ };
19871
+ } catch (error) {
19872
+ return {
19873
+ success: false,
19874
+ error: /* @__PURE__ */ new Error(`Failed to parse Reasonix config TOML: ${formatError(error)}`)
19875
+ };
19876
+ }
19877
+ }
19878
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
19879
+ return new ReasonixPermissions({
19880
+ outputRoot,
19881
+ relativeDirPath,
19882
+ relativeFilePath,
19883
+ fileContent: smolToml.stringify({}),
19884
+ validate: false
19885
+ });
19886
+ }
19887
+ };
19888
+ /**
19889
+ * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
19890
+ */
19891
+ function convertRulesyncToReasonixPermissions(config) {
19892
+ const allow = [];
19893
+ const ask = [];
19894
+ const deny = [];
19895
+ for (const [category, rules] of Object.entries(config.permission)) {
19896
+ const reasonixToolName = toReasonixToolName(category);
19897
+ for (const [pattern, action] of Object.entries(rules)) {
19898
+ const entry = buildReasonixPermissionEntry(reasonixToolName, pattern);
19899
+ switch (action) {
19900
+ case "allow":
19901
+ allow.push(entry);
19902
+ break;
19903
+ case "ask":
19904
+ ask.push(entry);
19905
+ break;
19906
+ case "deny":
19907
+ deny.push(entry);
19908
+ break;
19909
+ }
19910
+ }
19911
+ }
19912
+ return {
19913
+ allow,
19914
+ ask,
19915
+ deny
19916
+ };
19917
+ }
19918
+ /**
19919
+ * Convert Reasonix allow/ask/deny arrays to rulesync permissions config.
19920
+ */
19921
+ function convertReasonixToRulesyncPermissions(params) {
19922
+ const permission = {};
19923
+ const processEntries = (entries, action) => {
19924
+ for (const entry of entries) {
19925
+ const { toolName, pattern } = parseReasonixPermissionEntry(entry);
17880
19926
  const canonical = toCanonicalToolName$2(toolName);
17881
19927
  if (!permission[canonical]) permission[canonical] = {};
17882
19928
  permission[canonical][pattern] = action;
@@ -17989,13 +20035,13 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
17989
20035
  } catch (error) {
17990
20036
  throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
17991
20037
  }
17992
- const config = isRecord$1(parsed) ? { ...parsed } : {};
20038
+ const config = isRecord(parsed) ? { ...parsed } : {};
17993
20039
  const toolPermissions = convertRulesyncToRovodevToolPermissions({
17994
20040
  config: rulesyncPermissions.getJson(),
17995
20041
  logger
17996
20042
  });
17997
20043
  config.toolPermissions = {
17998
- ...isRecord$1(config.toolPermissions) ? { ...config.toolPermissions } : {},
20044
+ ...isRecord(config.toolPermissions) ? { ...config.toolPermissions } : {},
17999
20045
  ...toolPermissions
18000
20046
  };
18001
20047
  return new RovodevPermissions({
@@ -18015,8 +20061,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
18015
20061
  } catch (error) {
18016
20062
  throw new Error(`Failed to parse Rovodev permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18017
20063
  }
18018
- const config = isRecord$1(parsed) ? parsed : {};
18019
- const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
20064
+ const config = isRecord(parsed) ? parsed : {};
20065
+ const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
18020
20066
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
18021
20067
  }
18022
20068
  validate() {
@@ -18094,11 +20140,11 @@ function convertBashRules(rules) {
18094
20140
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
18095
20141
  const permission = {};
18096
20142
  const bash = toolPermissions.bash;
18097
- if (isRecord$1(bash)) {
20143
+ if (isRecord(bash)) {
18098
20144
  const bashRules = {};
18099
20145
  if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
18100
20146
  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;
20147
+ for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
18102
20148
  }
18103
20149
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
18104
20150
  }
@@ -18237,21 +20283,6 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
18237
20283
  }
18238
20284
  };
18239
20285
  /**
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
20286
  * Resolve the active Takt provider: the top-level `provider:` value, else the
18256
20287
  * sole key in `provider_profiles`, else the `claude` default.
18257
20288
  */
@@ -18573,8 +20604,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18573
20604
  config: rulesyncPermissions.getJson(),
18574
20605
  logger
18575
20606
  });
18576
- const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
18577
- const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
20607
+ const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
20608
+ const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
18578
20609
  const mergedAllow = uniq(allow.toSorted());
18579
20610
  const mergedDeny = uniq(deny.toSorted());
18580
20611
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -18599,8 +20630,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18599
20630
  } catch (error) {
18600
20631
  throw new Error(`Failed to parse Warp permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18601
20632
  }
18602
- const agents = isRecord$1(settings.agents) ? settings.agents : {};
18603
- const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
20633
+ const agents = isRecord(settings.agents) ? settings.agents : {};
20634
+ const profiles = isRecord(agents.profiles) ? agents.profiles : {};
18604
20635
  const config = convertWarpToRulesyncPermissions({
18605
20636
  allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
18606
20637
  deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
@@ -18951,6 +20982,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
18951
20982
  supportsImport: true
18952
20983
  }
18953
20984
  }],
20985
+ ["devin", {
20986
+ class: DevinPermissions,
20987
+ meta: {
20988
+ supportsProject: true,
20989
+ supportsGlobal: true,
20990
+ supportsImport: true
20991
+ }
20992
+ }],
18954
20993
  ["factorydroid", {
18955
20994
  class: FactorydroidPermissions,
18956
20995
  meta: {
@@ -18983,6 +21022,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
18983
21022
  supportsImport: true
18984
21023
  }
18985
21024
  }],
21025
+ ["junie", {
21026
+ class: JuniePermissions,
21027
+ meta: {
21028
+ supportsProject: true,
21029
+ supportsGlobal: true,
21030
+ supportsImport: true
21031
+ }
21032
+ }],
18986
21033
  ["kilo", {
18987
21034
  class: KiloPermissions,
18988
21035
  meta: {
@@ -19031,6 +21078,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
19031
21078
  supportsImport: true
19032
21079
  }
19033
21080
  }],
21081
+ ["reasonix", {
21082
+ class: ReasonixPermissions,
21083
+ meta: {
21084
+ supportsProject: true,
21085
+ supportsGlobal: true,
21086
+ supportsImport: true
21087
+ }
21088
+ }],
19034
21089
  ["rovodev", {
19035
21090
  class: RovodevPermissions,
19036
21091
  meta: {
@@ -19583,7 +21638,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
19583
21638
  opencode: z.optional(z.looseObject({
19584
21639
  "allowed-tools": z.optional(z.array(z.string())),
19585
21640
  license: z.optional(z.string()),
19586
- compatibility: z.optional(z.looseObject({})),
21641
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
19587
21642
  metadata: z.optional(z.looseObject({}))
19588
21643
  })),
19589
21644
  kilo: z.optional(z.looseObject({
@@ -19604,7 +21659,8 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
19604
21659
  })),
19605
21660
  copilotcli: z.optional(z.looseObject({
19606
21661
  license: z.optional(z.string()),
19607
- "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
21662
+ "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
21663
+ "argument-hint": z.optional(z.string())
19608
21664
  })),
19609
21665
  pi: z.optional(z.looseObject({
19610
21666
  "allowed-tools": z.optional(z.array(z.string())),
@@ -21358,7 +23414,11 @@ const CopilotSkillFrontmatterSchema = z.looseObject({
21358
23414
  });
21359
23415
  /**
21360
23416
  * Represents a GitHub Copilot skill directory.
21361
- * Skills are stored under the .github/skills directory with SKILL.md files.
23417
+ *
23418
+ * Copilot discovers project skills from `.github/skills/` and personal/global
23419
+ * skills from `~/.copilot/skills/`. Each skill is a directory containing a
23420
+ * `SKILL.md` file with `name`/`description` frontmatter.
23421
+ * https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
21362
23422
  */
21363
23423
  var CopilotSkill = class CopilotSkill extends ToolSkill {
21364
23424
  constructor({ outputRoot = process.cwd(), relativeDirPath = COPILOT_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -21380,7 +23440,7 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
21380
23440
  }
21381
23441
  }
21382
23442
  static getSettablePaths(options) {
21383
- if (options?.global) throw new Error("CopilotSkill does not support global mode.");
23443
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21384
23444
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21385
23445
  }
21386
23446
  getFrontmatter() {
@@ -21495,7 +23555,8 @@ const CopilotcliSkillFrontmatterSchema = z.looseObject({
21495
23555
  name: z.string(),
21496
23556
  description: z.string(),
21497
23557
  license: z.optional(z.string()),
21498
- "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
23558
+ "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
23559
+ "argument-hint": z.optional(z.string())
21499
23560
  });
21500
23561
  /**
21501
23562
  * Represents a GitHub Copilot CLI skill directory.
@@ -21525,7 +23586,7 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21525
23586
  }
21526
23587
  }
21527
23588
  static getSettablePaths(options) {
21528
- if (options?.global) return { relativeDirPath: COPILOTCLI_SKILLS_GLOBAL_DIR_PATH };
23589
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21529
23590
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21530
23591
  }
21531
23592
  getFrontmatter() {
@@ -21553,7 +23614,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21553
23614
  const frontmatter = this.getFrontmatter();
21554
23615
  const copilotcliSection = {
21555
23616
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
21556
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
23617
+ ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
23618
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
21557
23619
  };
21558
23620
  const rulesyncFrontmatter = {
21559
23621
  name: frontmatter.name,
@@ -21579,7 +23641,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21579
23641
  name: rulesyncFrontmatter.name,
21580
23642
  description: rulesyncFrontmatter.description,
21581
23643
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
21582
- ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] }
23644
+ ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
23645
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
21583
23646
  };
21584
23647
  return new CopilotcliSkill({
21585
23648
  outputRoot,
@@ -22805,8 +24868,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
22805
24868
  if (!result.success) throw result.error;
22806
24869
  }
22807
24870
  }
22808
- static getSettablePaths(options) {
22809
- if (options?.global) throw new Error("KiroSkill does not support global mode.");
24871
+ static getSettablePaths(_options) {
22810
24872
  return { relativeDirPath: KIRO_SKILLS_DIR_PATH };
22811
24873
  }
22812
24874
  getFrontmatter() {
@@ -22948,10 +25010,19 @@ const OpenCodeSkillFrontmatterSchema = z.looseObject({
22948
25010
  name: z.string(),
22949
25011
  description: z.string(),
22950
25012
  license: z.optional(z.string()),
22951
- compatibility: z.optional(z.looseObject({})),
25013
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
22952
25014
  metadata: z.optional(z.looseObject({})),
22953
25015
  "allowed-tools": z.optional(z.array(z.string()))
22954
25016
  });
25017
+ /**
25018
+ * Reads a top-level `compatibility` value from rulesync frontmatter, accepting
25019
+ * both the documented string form (e.g. `compatibility: opencode`) and the
25020
+ * legacy object form. Returns `undefined` for any other shape.
25021
+ */
25022
+ function readTopLevelCompatibility(value) {
25023
+ if (typeof value === "string") return value;
25024
+ if (typeof value === "object" && value !== null) return value;
25025
+ }
22955
25026
  var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
22956
25027
  constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
22957
25028
  super({
@@ -23028,7 +25099,7 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
23028
25099
  const opencodeSection = rulesyncFrontmatter.opencode;
23029
25100
  const looseTopLevel = rulesyncFrontmatter;
23030
25101
  const topLevelLicense = typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
23031
- const topLevelCompatibility = typeof looseTopLevel.compatibility === "object" && looseTopLevel.compatibility !== null ? looseTopLevel.compatibility : void 0;
25102
+ const topLevelCompatibility = readTopLevelCompatibility(looseTopLevel.compatibility);
23032
25103
  const topLevelMetadata = typeof looseTopLevel.metadata === "object" && looseTopLevel.metadata !== null ? looseTopLevel.metadata : void 0;
23033
25104
  const license = opencodeSection?.license ?? topLevelLicense;
23034
25105
  const compatibility = opencodeSection?.compatibility ?? topLevelCompatibility;
@@ -24382,7 +26453,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24382
26453
  meta: {
24383
26454
  supportsProject: true,
24384
26455
  supportsSimulated: false,
24385
- supportsGlobal: false
26456
+ supportsGlobal: true
24386
26457
  }
24387
26458
  }],
24388
26459
  ["copilotcli", {
@@ -24470,7 +26541,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24470
26541
  meta: {
24471
26542
  supportsProject: true,
24472
26543
  supportsSimulated: false,
24473
- supportsGlobal: false
26544
+ supportsGlobal: true
24474
26545
  }
24475
26546
  }],
24476
26547
  ["kiro-ide", {
@@ -24478,7 +26549,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24478
26549
  meta: {
24479
26550
  supportsProject: true,
24480
26551
  supportsSimulated: false,
24481
- supportsGlobal: false
26552
+ supportsGlobal: true
24482
26553
  }
24483
26554
  }],
24484
26555
  ["opencode", {
@@ -28566,7 +30637,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28566
30637
  class: KiroCliSubagent,
28567
30638
  meta: {
28568
30639
  supportsSimulated: false,
28569
- supportsGlobal: false,
30640
+ supportsGlobal: true,
28570
30641
  filePattern: "*.json"
28571
30642
  }
28572
30643
  }],
@@ -28574,7 +30645,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28574
30645
  class: KiroIdeSubagent,
28575
30646
  meta: {
28576
30647
  supportsSimulated: false,
28577
- supportsGlobal: false,
30648
+ supportsGlobal: true,
28578
30649
  filePattern: "*.md"
28579
30650
  }
28580
30651
  }],
@@ -31111,7 +33182,7 @@ const STRATEGIES = [
31111
33182
  * plus companion `globs`/`description` fields. (`.devin/rules/` is the
31112
33183
  * pre-rebrand legacy location the tool still reads.)
31113
33184
  * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
31114
- * `~/.codeium/windsurf/memories/global_rules.md` (unchanged by the rebrand).
33185
+ * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
31115
33186
  *
31116
33187
  * Trigger inference (when no explicit devin trigger is persisted):
31117
33188
  * - Specific globs (non wildcard) → glob
@@ -31138,8 +33209,8 @@ var DevinRule = class DevinRule extends ToolRule {
31138
33209
  }
31139
33210
  static getGlobalRootPath(excludeToolDir) {
31140
33211
  return {
31141
- relativeDirPath: buildToolPath(CODEIUM_DIR, WINDSURF_MEMORIES_SUBDIR, excludeToolDir),
31142
- relativeFilePath: DEVIN_GLOBAL_RULES_FILE_NAME
33212
+ relativeDirPath: buildToolPath(DEVIN_GLOBAL_CONFIG_DIR_PATH, ".", excludeToolDir),
33213
+ relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
31143
33214
  };
31144
33215
  }
31145
33216
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
@@ -31702,6 +33773,9 @@ var KiloRule = class KiloRule extends ToolRule {
31702
33773
  nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
31703
33774
  };
31704
33775
  }
33776
+ static getExtraSharedWritePaths({ global = false } = {}) {
33777
+ return global ? [] : [KiloMcp.getSettablePaths({ global: false })];
33778
+ }
31705
33779
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
31706
33780
  const paths = this.getSettablePaths({ global });
31707
33781
  if (relativeFilePath === paths.root.relativeFilePath) {
@@ -31963,6 +34037,9 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
31963
34037
  nonRoot: { relativeDirPath: buildToolPath(OPENCODE_DIR, "memories", excludeToolDir) }
31964
34038
  };
31965
34039
  }
34040
+ static getExtraSharedWritePaths({ global = false } = {}) {
34041
+ return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
34042
+ }
31966
34043
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
31967
34044
  const paths = this.getSettablePaths({ global });
31968
34045
  if (relativeFilePath === paths.root.relativeFilePath) {
@@ -32029,7 +34106,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32029
34106
  /**
32030
34107
  * Rule generator for Pi Coding Agent.
32031
34108
  *
32032
- * Pi loads instruction context only from the `AGENTS.md` / `CLAUDE.md` family —
34109
+ * Pi loads instruction context from the `AGENTS.md` / `CLAUDE.md` family —
32033
34110
  * the global `~/.pi/agent/AGENTS.md` plus files discovered by walking up the
32034
34111
  * directory tree from the current working directory. It does NOT resolve
32035
34112
  * `@`-imports or a TOON file list, and has no `.agents/memories/` concept, so
@@ -32040,6 +34117,15 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32040
34117
  * to map onto; their bodies are folded into the single root `AGENTS.md` by the
32041
34118
  * RulesProcessor (there is no separate non-root output location — `nonRoot` is
32042
34119
  * `undefined`). This mirrors the codexcli, grokcli, warp, and deepagents targets.
34120
+ *
34121
+ * Pi also loads two system-prompt instruction files that rulesync does NOT emit:
34122
+ * `.pi/SYSTEM.md` (global `~/.pi/agent/SYSTEM.md`) *replaces* the default system
34123
+ * prompt entirely, and `.pi/APPEND_SYSTEM.md` (global
34124
+ * `~/.pi/agent/APPEND_SYSTEM.md`) *appends* to it. rulesync's rules model only
34125
+ * routes a designated `root` rule to a single context file and has no convention
34126
+ * for marking a rule as "replace" vs "append" the system prompt, so these
34127
+ * surfaces are documented in docs/reference/file-formats.md and left to be
34128
+ * authored by hand rather than mapped to a speculative new frontmatter flag.
32043
34129
  */
32044
34130
  var PiRule = class PiRule extends ToolRule {
32045
34131
  constructor({ fileContent, root, ...rest }) {
@@ -32630,6 +34716,37 @@ var RovodevRule = class RovodevRule extends ToolRule {
32630
34716
  toolTarget: "rovodev"
32631
34717
  });
32632
34718
  }
34719
+ /**
34720
+ * Mirror the primary `.rovodev/AGENTS.md` root rule to `./AGENTS.md` so project
34721
+ * memory stays discoverable at the repo root. Empty when `rootRule` is not the primary root.
34722
+ */
34723
+ static getRootMirrorFiles({ outputRoot, rootRule, content }) {
34724
+ if (!(rootRule instanceof RovodevRule)) return [];
34725
+ const primary = this.getSettablePaths({ global: false }).root;
34726
+ if (rootRule.getRelativeDirPath() !== primary.relativeDirPath || rootRule.getRelativeFilePath() !== primary.relativeFilePath) return [];
34727
+ return [new RovodevRule({
34728
+ outputRoot,
34729
+ relativeDirPath: ".",
34730
+ relativeFilePath: ROVODEV_RULE_FILE_NAME,
34731
+ fileContent: content,
34732
+ validate: true,
34733
+ root: true
34734
+ })];
34735
+ }
34736
+ /**
34737
+ * Globs for mirror deletion: the `./AGENTS.md` mirror (`mirrorGlob`) is deleted
34738
+ * only when the primary `.rovodev/AGENTS.md` (`primaryGlob`) still exists.
34739
+ */
34740
+ static getRootMirrorDeletionGlobs({ outputRoot }) {
34741
+ return {
34742
+ primaryGlob: join(outputRoot, ROVODEV_DIR, ROVODEV_RULE_FILE_NAME),
34743
+ mirrorGlob: join(outputRoot, ROVODEV_RULE_FILE_NAME)
34744
+ };
34745
+ }
34746
+ /** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
34747
+ static getLocalRootDeletionGlob({ outputRoot, fileName }) {
34748
+ return join(outputRoot, fileName);
34749
+ }
32633
34750
  };
32634
34751
  //#endregion
32635
34752
  //#region src/features/rules/takt-rule.ts
@@ -33352,7 +35469,7 @@ var RulesProcessor = class extends FeatureProcessor {
33352
35469
  });
33353
35470
  this.applyRootRuleSections({
33354
35471
  toolRules,
33355
- meta
35472
+ factory
33356
35473
  });
33357
35474
  return [...toolRules, ...extraFiles];
33358
35475
  }
@@ -33414,31 +35531,16 @@ var RulesProcessor = class extends FeatureProcessor {
33414
35531
  * reference and conventions sections to the root rule content. Mutates the
33415
35532
  * root rule in place.
33416
35533
  */
33417
- applyRootRuleSections({ toolRules, meta }) {
35534
+ applyRootRuleSections({ toolRules, factory }) {
35535
+ const { meta } = factory;
33418
35536
  const rootRule = toolRules.find((rule) => rule.isRoot());
33419
35537
  if (!rootRule) return;
33420
35538
  const newContent = this.generateReferenceSectionFromMeta(meta, toolRules) + (!meta.createsSeparateConventionsRule && meta.additionalConventions ? this.generateAdditionalConventionsSectionFromMeta(meta) : "") + rootRule.getFileContent();
33421
35539
  rootRule.setFileContent(newContent);
33422
- if (meta.mirrorsRootToAgentsMd && !this.global) this.mirrorRootRuleToAgentsMd({
33423
- toolRules,
35540
+ if (meta.mirrorsRootToAgentsMd && !this.global && factory.class.getRootMirrorFiles) toolRules.push(...factory.class.getRootMirrorFiles({
35541
+ outputRoot: this.outputRoot,
33424
35542
  rootRule,
33425
35543
  content: newContent
33426
- });
33427
- }
33428
- /**
33429
- * Mirror the primary root rule to a project-root `AGENTS.md` for tools whose
33430
- * primary root lives in a subdirectory (rovodev: `.rovodev/AGENTS.md`).
33431
- */
33432
- mirrorRootRuleToAgentsMd({ toolRules, rootRule, content }) {
33433
- if (!(rootRule instanceof RovodevRule)) return;
33434
- const primary = RovodevRule.getSettablePaths({ global: false }).root;
33435
- if (rootRule.getRelativeDirPath() === primary.relativeDirPath && rootRule.getRelativeFilePath() === primary.relativeFilePath) toolRules.push(new RovodevRule({
33436
- outputRoot: this.outputRoot,
33437
- relativeDirPath: ".",
33438
- relativeFilePath: "AGENTS.md",
33439
- fileContent: content,
33440
- validate: true,
33441
- root: true
33442
35544
  }));
33443
35545
  }
33444
35546
  buildSkillList(skillClass) {
@@ -33664,15 +35766,19 @@ As this project's AI coding tool, you must follow the additional conventions bel
33664
35766
  const localRootToolRules = await (async () => {
33665
35767
  if (!forDeletion || this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
33666
35768
  const fileName = factory.meta.localRootFileName;
33667
- if (factory.class === RovodevRule) return buildDeletionRulesFromPaths(await findFilesByGlobs(join(this.outputRoot, fileName)));
35769
+ if (factory.class.getLocalRootDeletionGlob) return buildDeletionRulesFromPaths(await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
35770
+ outputRoot: this.outputRoot,
35771
+ fileName
35772
+ })));
33668
35773
  if (!settablePaths.root) return [];
33669
35774
  return buildDeletionRulesFromPaths(await findFilesWithFallback(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, fileName)));
33670
35775
  })();
33671
35776
  this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files for deletion`);
33672
35777
  const rootMirrorDeletionRules = await (async () => {
33673
- if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || factory.class !== RovodevRule) return [];
33674
- if ((await findFilesByGlobs(join(this.outputRoot, ".rovodev", "AGENTS.md"))).length === 0) return [];
33675
- return buildDeletionRulesFromPaths(await findFilesByGlobs(join(this.outputRoot, "AGENTS.md")));
35778
+ if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || !factory.class.getRootMirrorDeletionGlobs) return [];
35779
+ const { primaryGlob, mirrorGlob } = factory.class.getRootMirrorDeletionGlobs({ outputRoot: this.outputRoot });
35780
+ if ((await findFilesByGlobs(primaryGlob)).length === 0) return [];
35781
+ return buildDeletionRulesFromPaths(await findFilesByGlobs(mirrorGlob));
33676
35782
  })();
33677
35783
  const nonRootToolRules = await (async () => {
33678
35784
  if (!settablePaths.nonRoot) return [];
@@ -34121,63 +36227,228 @@ function warnUnsupportedTargets(params) {
34121
36227
  async function checkRulesyncDirExists(params) {
34122
36228
  return fileExists(join(params.inputRoot, RULESYNC_RELATIVE_DIR_PATH));
34123
36229
  }
36230
+ function dependsOnReachable(byId, from, target) {
36231
+ const seen = /* @__PURE__ */ new Set();
36232
+ const stack = [from];
36233
+ while (stack.length > 0) {
36234
+ const current = stack.pop();
36235
+ if (current === void 0 || seen.has(current)) continue;
36236
+ seen.add(current);
36237
+ if (current === target) return true;
36238
+ for (const dep of byId.get(current)?.dependsOn ?? []) stack.push(dep);
36239
+ }
36240
+ return false;
36241
+ }
36242
+ function assertSharedFilesOrdered(steps, byId) {
36243
+ const writersByFile = /* @__PURE__ */ new Map();
36244
+ for (const step of steps) for (const file of step.writesSharedFile ?? []) writersByFile.set(file, [...writersByFile.get(file) ?? [], step.id]);
36245
+ for (const [file, writers] of writersByFile) for (let i = 0; i < writers.length; i++) for (let j = i + 1; j < writers.length; j++) {
36246
+ const a = writers[i];
36247
+ const b = writers[j];
36248
+ 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.`);
36249
+ }
36250
+ }
36251
+ /**
36252
+ * Topologically sort generation steps and reject ordering hazards: a shared file
36253
+ * with two writers not ordered by `dependsOn` (a silent data-loss trap), an
36254
+ * unknown dependency, or a cycle. Reordering `steps` stays safe as a result.
36255
+ *
36256
+ * @throws Error if a shared file has unordered writers, a dependency is unknown,
36257
+ * or the dependency graph contains a cycle.
36258
+ */
36259
+ function resolveExecutionOrder(steps) {
36260
+ const byId = new Map(steps.map((step) => [step.id, step]));
36261
+ assertSharedFilesOrdered(steps, byId);
36262
+ const unresolvedDeps = new Map(steps.map((step) => [step.id, 0]));
36263
+ const dependents = /* @__PURE__ */ new Map();
36264
+ for (const step of steps) for (const dep of step.dependsOn ?? []) {
36265
+ if (!byId.has(dep)) throw new Error(`Generation step '${step.id}' depends on unknown step '${dep}'.`);
36266
+ unresolvedDeps.set(step.id, (unresolvedDeps.get(step.id) ?? 0) + 1);
36267
+ dependents.set(dep, [...dependents.get(dep) ?? [], step.id]);
36268
+ }
36269
+ const ready = steps.filter((step) => (unresolvedDeps.get(step.id) ?? 0) === 0).map((step) => step.id);
36270
+ const ordered = [];
36271
+ while (ready.length > 0) {
36272
+ const id = ready.shift();
36273
+ ordered.push(byId.get(id));
36274
+ for (const dependent of dependents.get(id) ?? []) {
36275
+ const next = (unresolvedDeps.get(dependent) ?? 0) - 1;
36276
+ unresolvedDeps.set(dependent, next);
36277
+ if (next === 0) ready.push(dependent);
36278
+ }
36279
+ }
36280
+ if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
36281
+ return ordered;
36282
+ }
36283
+ /**
36284
+ * The static shape of the generation step graph: which steps write which shared
36285
+ * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
36286
+ * for those writers. Exported (separately from the `run` closures, which need a
36287
+ * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
36288
+ * tested directly against the real graph rather than a hand-copied one. Readonly
36289
+ * so a consumer can't mutate this module-level singleton and affect every
36290
+ * subsequent `generate()` call in the process.
36291
+ */
36292
+ const GENERATION_STEP_GRAPH = [
36293
+ {
36294
+ id: "ignore",
36295
+ writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
36296
+ },
36297
+ {
36298
+ id: "mcp",
36299
+ writesSharedFile: [
36300
+ ".amp/settings.json",
36301
+ ".augment/settings.json",
36302
+ ".codex/config.toml",
36303
+ ".config/amp/settings.json",
36304
+ ".config/devin/config.json",
36305
+ ".config/opencode/opencode.json",
36306
+ ".config/zed/settings.json",
36307
+ ".devin/config.json",
36308
+ ".grok/config.toml",
36309
+ ".hermes/config.yaml",
36310
+ ".qwen/settings.json",
36311
+ ".reasonix/config.toml",
36312
+ ".takt/config.yaml",
36313
+ ".vibe/config.toml",
36314
+ ".zed/settings.json",
36315
+ "kilo.json",
36316
+ "opencode.json",
36317
+ "reasonix.toml"
36318
+ ],
36319
+ dependsOn: ["ignore"]
36320
+ },
36321
+ { id: "commands" },
36322
+ { id: "subagents" },
36323
+ { id: "skills" },
36324
+ {
36325
+ id: "hooks",
36326
+ writesSharedFile: [
36327
+ ".augment/settings.json",
36328
+ ".claude/settings.json",
36329
+ ".codex/config.toml",
36330
+ ".config/devin/config.json",
36331
+ ".hermes/config.yaml",
36332
+ ".kiro/agents/default.json",
36333
+ ".qwen/settings.json",
36334
+ ".vibe/config.toml"
36335
+ ],
36336
+ dependsOn: ["ignore", "mcp"]
36337
+ },
36338
+ {
36339
+ id: "permissions",
36340
+ writesSharedFile: [
36341
+ ".amp/settings.json",
36342
+ ".augment/settings.json",
36343
+ ".claude/settings.json",
36344
+ ".codex/config.toml",
36345
+ ".config/amp/settings.json",
36346
+ ".config/devin/config.json",
36347
+ ".config/opencode/opencode.json",
36348
+ ".config/zed/settings.json",
36349
+ ".devin/config.json",
36350
+ ".grok/config.toml",
36351
+ ".hermes/config.yaml",
36352
+ ".kiro/agents/default.json",
36353
+ ".qwen/settings.json",
36354
+ ".reasonix/config.toml",
36355
+ ".takt/config.yaml",
36356
+ ".vibe/config.toml",
36357
+ ".zed/settings.json",
36358
+ "opencode.json",
36359
+ "reasonix.toml"
36360
+ ],
36361
+ dependsOn: [
36362
+ "ignore",
36363
+ "hooks",
36364
+ "mcp"
36365
+ ]
36366
+ },
36367
+ {
36368
+ id: "rules",
36369
+ writesSharedFile: ["kilo.json", "opencode.json"],
36370
+ dependsOn: [
36371
+ "mcp",
36372
+ "skills",
36373
+ "permissions"
36374
+ ]
36375
+ }
36376
+ ];
34124
36377
  /**
34125
36378
  * Generate configuration files for AI tools.
34126
36379
  * @throws Error if generation fails
34127
36380
  */
34128
36381
  async function generate(params) {
34129
36382
  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;
36383
+ let skillsResult;
36384
+ const runners = {
36385
+ ignore: () => generateIgnoreCore({
36386
+ config,
36387
+ logger
36388
+ }),
36389
+ mcp: () => generateMcpCore({
36390
+ config,
36391
+ logger
36392
+ }),
36393
+ commands: () => generateCommandsCore({
36394
+ config,
36395
+ logger
36396
+ }),
36397
+ subagents: () => generateSubagentsCore({
36398
+ config,
36399
+ logger
36400
+ }),
36401
+ skills: async () => {
36402
+ skillsResult = await generateSkillsCore({
36403
+ config,
36404
+ logger
36405
+ });
36406
+ return skillsResult;
36407
+ },
36408
+ hooks: () => generateHooksCore({
36409
+ config,
36410
+ logger
36411
+ }),
36412
+ permissions: () => generatePermissionsCore({
36413
+ config,
36414
+ logger
36415
+ }),
36416
+ rules: () => generateRulesCore({
36417
+ config,
36418
+ logger,
36419
+ skills: skillsResult?.skills
36420
+ })
36421
+ };
36422
+ const orderedSteps = resolveExecutionOrder(GENERATION_STEP_GRAPH.map((meta) => ({
36423
+ ...meta,
36424
+ run: runners[meta.id]
36425
+ })));
36426
+ const resultsById = /* @__PURE__ */ new Map();
36427
+ for (const step of orderedSteps) resultsById.set(step.id, await step.run());
36428
+ if (!skillsResult) throw new Error("Skills generation step did not run.");
36429
+ const get = (id) => {
36430
+ const result = resultsById.get(id);
36431
+ if (!result) throw new Error(`Missing generation result for step '${id}'.`);
36432
+ return result;
36433
+ };
36434
+ const hasDiff = orderedSteps.some((step) => get(step.id).hasDiff);
34164
36435
  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,
36436
+ rulesCount: get("rules").count,
36437
+ rulesPaths: get("rules").paths,
36438
+ ignoreCount: get("ignore").count,
36439
+ ignorePaths: get("ignore").paths,
36440
+ mcpCount: get("mcp").count,
36441
+ mcpPaths: get("mcp").paths,
36442
+ commandsCount: get("commands").count,
36443
+ commandsPaths: get("commands").paths,
36444
+ subagentsCount: get("subagents").count,
36445
+ subagentsPaths: get("subagents").paths,
34175
36446
  skillsCount: skillsResult.count,
34176
36447
  skillsPaths: skillsResult.paths,
34177
- hooksCount: hooksResult.count,
34178
- hooksPaths: hooksResult.paths,
34179
- permissionsCount: permissionsResult.count,
34180
- permissionsPaths: permissionsResult.paths,
36448
+ hooksCount: get("hooks").count,
36449
+ hooksPaths: get("hooks").paths,
36450
+ permissionsCount: get("permissions").count,
36451
+ permissionsPaths: get("permissions").paths,
34181
36452
  skills: skillsResult.skills,
34182
36453
  hasDiff
34183
36454
  };
@@ -34794,4 +37065,4 @@ async function importPermissionsCore(params) {
34794
37065
  //#endregion
34795
37066
  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
37067
 
34797
- //# sourceMappingURL=import-zmpFGK87.js.map
37068
+ //# sourceMappingURL=import-DywcWsgJ.js.map