rulesync 16.0.0 → 16.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -127,27 +127,27 @@ const isFeatureValueEnabled = (value) => {
127
127
  };
128
128
  //#endregion
129
129
  //#region src/constants/rulesync-paths.ts
130
- const { join: join$275 } = node_path.posix;
130
+ const { join: join$277 } = node_path.posix;
131
131
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
132
132
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
133
133
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
134
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "rules");
135
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$275(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
136
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "commands");
137
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "subagents");
138
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "checks");
139
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
140
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
141
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
142
- join$275(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
143
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
144
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
134
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "rules");
135
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$277(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
136
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "commands");
137
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "subagents");
138
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "checks");
139
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
140
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
141
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
142
+ join$277(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
143
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
144
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
145
145
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
146
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
146
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
147
147
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
148
148
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
149
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "skills");
150
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$275(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
149
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "skills");
150
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$277(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
151
151
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
152
152
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
153
153
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -293,6 +293,9 @@ const commandsProcessorToolTargetTuple = [
293
293
  const subagentsProcessorToolTargetTuple = [
294
294
  "kilo",
295
295
  "agentsmd",
296
+ "antigravity-cli",
297
+ "antigravity-ide",
298
+ "antigravity-plugin",
296
299
  "augmentcode",
297
300
  "claudecode",
298
301
  "claudecode-plugin",
@@ -424,6 +427,7 @@ const permissionsProcessorToolTargetTuple = [
424
427
  ];
425
428
  const checksProcessorToolTargetTuple = [
426
429
  "amp",
430
+ "cursor",
427
431
  "hermesagent",
428
432
  "takt"
429
433
  ];
@@ -2134,7 +2138,10 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
2134
2138
  model: zod_mini.z.optional(safeString),
2135
2139
  args: zod_mini.z.optional(zod_mini.z.array(safeString)),
2136
2140
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
2137
- if: zod_mini.z.optional(safeString)
2141
+ if: zod_mini.z.optional(safeString),
2142
+ commandWindows: zod_mini.z.optional(safeString),
2143
+ asyncRewake: zod_mini.z.optional(zod_mini.z.boolean()),
2144
+ continueOnBlock: zod_mini.z.optional(zod_mini.z.boolean())
2138
2145
  });
2139
2146
  /**
2140
2147
  * All canonical hook event names.
@@ -2188,6 +2195,7 @@ const HOOK_EVENTS = [
2188
2195
  "configChange",
2189
2196
  "cwdChanged",
2190
2197
  "fileChanged",
2198
+ "directoryAdded",
2191
2199
  "elicitation",
2192
2200
  "elicitationResult"
2193
2201
  ];
@@ -2249,6 +2257,7 @@ const CLAUDE_HOOK_EVENTS = [
2249
2257
  "configChange",
2250
2258
  "cwdChanged",
2251
2259
  "fileChanged",
2260
+ "directoryAdded",
2252
2261
  "postCompact",
2253
2262
  "elicitation",
2254
2263
  "elicitationResult"
@@ -2281,7 +2290,15 @@ const DEVIN_HOOK_EVENTS = [
2281
2290
  "permissionRequest",
2282
2291
  "postCompact"
2283
2292
  ];
2284
- /** Hook events supported by OpenCode. */
2293
+ /**
2294
+ * Hook events supported by OpenCode.
2295
+ *
2296
+ * `preCompact` maps to `experimental.session.compacting`, which the plugin docs
2297
+ * document as a named `(input, output)` hook rather than an `event.type`
2298
+ * dispatch; the other entries are all generic events.
2299
+ *
2300
+ * @see https://opencode.ai/docs/plugins/
2301
+ */
2285
2302
  const OPENCODE_HOOK_EVENTS = [
2286
2303
  "sessionStart",
2287
2304
  "preToolUse",
@@ -2289,9 +2306,19 @@ const OPENCODE_HOOK_EVENTS = [
2289
2306
  "stop",
2290
2307
  "afterFileEdit",
2291
2308
  "afterShellExecution",
2292
- "permissionRequest"
2309
+ "permissionRequest",
2310
+ "preCompact",
2311
+ "postCompact",
2312
+ "afterError",
2313
+ "fileChanged"
2293
2314
  ];
2294
- /** Hook events supported by Kilo. (Currently identical to OpenCode) */
2315
+ /**
2316
+ * Hook events supported by Kilo. Identical to OpenCode: Kilo's plugin docs list
2317
+ * the same event surface, including `session.compacted`, `session.error`,
2318
+ * `file.watcher.updated` and the experimental compaction hook.
2319
+ *
2320
+ * @see https://kilo.ai/docs/automate/extending/plugins
2321
+ */
2295
2322
  const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS;
2296
2323
  /**
2297
2324
  * Hook events supported by Pi Coding Agent, bridged through a generated
@@ -2358,7 +2385,8 @@ const COPILOT_HOOK_EVENTS = [
2358
2385
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
2359
2386
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
2360
2387
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
2361
- * `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
2388
+ * `notification`, `userPromptTransformed` ← `userPromptExpansion`,
2389
+ * `preMcpToolCall` ← `beforeMCPExecution`.
2362
2390
  *
2363
2391
  * `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
2364
2392
  * v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
@@ -2380,6 +2408,7 @@ const COPILOTCLI_HOOK_EVENTS = [
2380
2408
  "preCompact",
2381
2409
  "permissionRequest",
2382
2410
  "notification",
2411
+ "userPromptExpansion",
2383
2412
  "beforeMCPExecution"
2384
2413
  ];
2385
2414
  /**
@@ -2424,6 +2453,7 @@ const DEEPAGENTS_HOOK_EVENTS = [
2424
2453
  /** Hook events supported by Codex CLI. */
2425
2454
  const CODEXCLI_HOOK_EVENTS = [
2426
2455
  "sessionStart",
2456
+ "sessionEnd",
2427
2457
  "preToolUse",
2428
2458
  "postToolUse",
2429
2459
  "beforeSubmitPrompt",
@@ -2858,6 +2888,7 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
2858
2888
  configChange: "ConfigChange",
2859
2889
  cwdChanged: "CwdChanged",
2860
2890
  fileChanged: "FileChanged",
2891
+ directoryAdded: "DirectoryAdded",
2861
2892
  postCompact: "PostCompact",
2862
2893
  elicitation: "Elicitation",
2863
2894
  elicitationResult: "ElicitationResult"
@@ -2979,7 +3010,11 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
2979
3010
  stop: "session.idle",
2980
3011
  afterFileEdit: "file.edited",
2981
3012
  afterShellExecution: "command.executed",
2982
- permissionRequest: "permission.asked"
3013
+ permissionRequest: "permission.asked",
3014
+ preCompact: "experimental.session.compacting",
3015
+ postCompact: "session.compacted",
3016
+ afterError: "session.error",
3017
+ fileChanged: "file.watcher.updated"
2983
3018
  };
2984
3019
  /**
2985
3020
  * Map canonical camelCase event names to Kilo dot-notation.
@@ -3055,6 +3090,7 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
3055
3090
  preCompact: "preCompact",
3056
3091
  permissionRequest: "permissionRequest",
3057
3092
  notification: "notification",
3093
+ userPromptExpansion: "userPromptTransformed",
3058
3094
  beforeMCPExecution: "preMcpToolCall"
3059
3095
  };
3060
3096
  /** Map GitHub Copilot CLI event names back to canonical camelCase. */
@@ -3064,6 +3100,7 @@ const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
3064
3100
  */
3065
3101
  const CANONICAL_TO_CODEXCLI_EVENT_NAMES = {
3066
3102
  sessionStart: "SessionStart",
3103
+ sessionEnd: "SessionEnd",
3067
3104
  preToolUse: "PreToolUse",
3068
3105
  postToolUse: "PostToolUse",
3069
3106
  beforeSubmitPrompt: "UserPromptSubmit",
@@ -3486,6 +3523,25 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
3486
3523
  //#endregion
3487
3524
  //#region src/types/mcp.ts
3488
3525
  const EnvVarNameSchema = zod_mini.z.string().check((0, zod_mini.refine)((value) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(value), "envVars entries must be valid environment variable names"));
3526
+ /**
3527
+ * One `envVars` entry. A bare name reads the variable from Codex's own
3528
+ * environment; the object form names the environment to read it from, where
3529
+ * `source = "remote"` reads from the remote executor environment.
3530
+ * @see https://learn.chatgpt.com/docs/extend/mcp
3531
+ */
3532
+ const EnvVarEntrySchema = zod_mini.z.union([EnvVarNameSchema, zod_mini.z.strictObject({
3533
+ name: EnvVarNameSchema,
3534
+ source: zod_mini.z.optional(zod_mini.z.enum(["local", "remote"]))
3535
+ })]);
3536
+ /**
3537
+ * Whether a value is usable as `envVars`. Applied in both directions by the
3538
+ * codex adapter, so an entry read out of somebody's `config.toml` can never be
3539
+ * imported into a `.rulesync/mcp.jsonc` that the next generate would refuse to
3540
+ * parse.
3541
+ */
3542
+ function isEnvVarEntryArray(value) {
3543
+ return Array.isArray(value) && value.every((entry) => EnvVarEntrySchema.safeParse(entry).success);
3544
+ }
3489
3545
  const McpServerSchema = zod_mini.z.looseObject({
3490
3546
  type: zod_mini.z.optional(zod_mini.z.enum([
3491
3547
  "local",
@@ -3500,7 +3556,8 @@ const McpServerSchema = zod_mini.z.looseObject({
3500
3556
  url: zod_mini.z.optional(zod_mini.z.string()),
3501
3557
  httpUrl: zod_mini.z.optional(zod_mini.z.string()),
3502
3558
  env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
3503
- envVars: zod_mini.z.optional(zod_mini.z.array(EnvVarNameSchema)),
3559
+ envVars: zod_mini.z.optional(zod_mini.z.array(EnvVarEntrySchema)),
3560
+ experimentalEnvironment: zod_mini.z.optional(zod_mini.z.string()),
3504
3561
  disabled: zod_mini.z.optional(zod_mini.z.boolean()),
3505
3562
  networkTimeout: zod_mini.z.optional(zod_mini.z.number()),
3506
3563
  timeout: zod_mini.z.optional(zod_mini.z.number()),
@@ -3666,6 +3723,8 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3666
3723
  "description",
3667
3724
  "exposed",
3668
3725
  "envVars",
3726
+ "experimentalEnvironment",
3727
+ "experimental_environment",
3669
3728
  "enabled"
3670
3729
  ])];
3671
3730
  }));
@@ -3968,12 +4027,23 @@ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_m
3968
4027
  * authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
3969
4028
  * ignored here (rulesync owns them).
3970
4029
  *
4030
+ * `sandbox` is the sibling top-level settings subtree that governs the sandbox
4031
+ * Claude Code runs commands in (`sandbox.network.*`, `sandbox.filesystem.*`,
4032
+ * `sandbox.credentials`, `sandbox.allowAppleEvents`, ...). It has no canonical
4033
+ * permission category either — it constrains how a permitted command runs
4034
+ * rather than which commands are permitted — so it is a loose passthrough on
4035
+ * the same terms, merged into the top level of `.claude/settings.json`.
4036
+ *
3971
4037
  * @example
3972
4038
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
4039
+ * @example
4040
+ * { "sandbox": { "network": { "allowedDomains": ["example.com"], "strictAllowlist": true } } }
4041
+ * @see https://code.claude.com/docs/en/sandboxing
3973
4042
  */
3974
4043
  const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
3975
4044
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
3976
- permissions: zod_mini.z.optional(zod_mini.z.looseObject({}))
4045
+ permissions: zod_mini.z.optional(zod_mini.z.looseObject({})),
4046
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
3977
4047
  });
3978
4048
  /**
3979
4049
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
@@ -4246,19 +4316,24 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4246
4316
  });
4247
4317
  /**
4248
4318
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4249
- * `settings.json` carries two global autonomy/sandbox knobs outside the
4319
+ * `settings.json` carries four global autonomy/sandbox knobs outside the
4250
4320
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4251
4321
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4252
- * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
4253
- * agent-run commands to OS containment). Antigravity applies the allow/deny
4322
+ * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
4323
+ * agent-run commands to OS containment), `artifactReviewPolicy` (whether the
4324
+ * agent's artifact changes are gated on a review prompt — `asks-for-review`
4325
+ * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4326
+ * (a boolean, off by default, letting the agent read or write files outside the
4327
+ * active workspace roots). Antigravity applies the allow/deny
4254
4328
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4255
4329
  * authors these keys verbatim — no precedence modeling is needed on our side.
4256
4330
  * Fields placed here are merged onto the top level of
4257
4331
  * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
4258
4332
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4259
4333
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4260
- * Verified against https://antigravity.google/docs/cli/reference and
4261
- * https://antigravity.google/docs/cli/sandbox.
4334
+ * Verified against https://antigravity.google/docs/cli/reference,
4335
+ * https://antigravity.google/docs/cli/sandbox and
4336
+ * https://antigravity.google/docs/cli/settings.
4262
4337
  *
4263
4338
  * @example
4264
4339
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
@@ -4271,7 +4346,13 @@ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
4271
4346
  "always-proceed",
4272
4347
  "strict"
4273
4348
  ])),
4274
- enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean())
4349
+ enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean()),
4350
+ artifactReviewPolicy: zod_mini.z.optional(zod_mini.z.enum([
4351
+ "asks-for-review",
4352
+ "agent-decides",
4353
+ "always-proceed"
4354
+ ])),
4355
+ allowNonWorkspaceAccess: zod_mini.z.optional(zod_mini.z.boolean())
4275
4356
  });
4276
4357
  /**
4277
4358
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -4675,11 +4756,14 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
4675
4756
  description: zod_mini.z.optional(zod_mini.z.string()),
4676
4757
  globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
4677
4758
  })),
4678
- copilot: zod_mini.z.optional(zod_mini.z.looseObject({ excludeAgent: zod_mini.z.optional(zod_mini.z.union([
4679
- zod_mini.z.literal("code-review"),
4680
- zod_mini.z.literal("cloud-agent"),
4681
- zod_mini.z.literal("coding-agent")
4682
- ])) })),
4759
+ copilot: zod_mini.z.optional(zod_mini.z.looseObject({
4760
+ excludeAgent: zod_mini.z.optional(zod_mini.z.union([
4761
+ zod_mini.z.literal("code-review"),
4762
+ zod_mini.z.literal("cloud-agent"),
4763
+ zod_mini.z.literal("coding-agent")
4764
+ ])),
4765
+ name: zod_mini.z.optional(zod_mini.z.string())
4766
+ })),
4683
4767
  antigravity: zod_mini.z.optional(zod_mini.z.looseObject({
4684
4768
  trigger: zod_mini.z.optional(zod_mini.z.string()),
4685
4769
  globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
@@ -4887,6 +4971,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
4887
4971
  arguments: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
4888
4972
  context: zod_mini.z.optional(zod_mini.z.string()),
4889
4973
  agent: zod_mini.z.optional(zod_mini.z.string()),
4974
+ background: zod_mini.z.optional(zod_mini.z.boolean()),
4890
4975
  hooks: zod_mini.z.optional(zod_mini.z.looseObject({})),
4891
4976
  shell: zod_mini.z.optional(zod_mini.z.string()),
4892
4977
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
@@ -4938,7 +5023,9 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
4938
5023
  copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
4939
5024
  license: zod_mini.z.optional(zod_mini.z.string()),
4940
5025
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
4941
- "argument-hint": zod_mini.z.optional(zod_mini.z.string())
5026
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
5027
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
5028
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
4942
5029
  })),
4943
5030
  pi: zod_mini.z.optional(zod_mini.z.looseObject({
4944
5031
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
@@ -5224,6 +5311,18 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
5224
5311
  return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
5225
5312
  }
5226
5313
  //#endregion
5314
+ //#region src/constants/cursor-paths.ts
5315
+ const CURSOR_DIR = ".cursor";
5316
+ const CURSOR_COMMANDS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "commands");
5317
+ const CURSOR_SKILLS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "skills");
5318
+ const CURSOR_AGENTS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "agents");
5319
+ const CURSOR_BUGBOT_FILE_NAME = "BUGBOT.md";
5320
+ const CURSOR_MCP_FILE_NAME = "mcp.json";
5321
+ const CURSOR_HOOKS_FILE_NAME = "hooks.json";
5322
+ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5323
+ const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5324
+ const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5325
+ //#endregion
5227
5326
  //#region src/constants/takt-paths.ts
5228
5327
  const TAKT_DIR = ".takt";
5229
5328
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5674,6 +5773,235 @@ var AmpCheck = class AmpCheck extends ToolCheck {
5674
5773
  }
5675
5774
  };
5676
5775
  //#endregion
5776
+ //#region src/features/checks/check-slug.ts
5777
+ /**
5778
+ * Turn a name that came out of a tool's own config file into one safe to use as
5779
+ * a `.rulesync/checks/<name>.md` file name. Shared by the adapters whose checks
5780
+ * collapse into a single file, since the name they read back is whatever the
5781
+ * user wrote there.
5782
+ */
5783
+ function slugifyCheckName(value) {
5784
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5785
+ }
5786
+ //#endregion
5787
+ //#region src/features/checks/cursor-check.ts
5788
+ /**
5789
+ * Marks where one check starts inside the single instruction file. Bugbot reads
5790
+ * the file as prose, and an HTML comment is invisible in rendered Markdown, so
5791
+ * the marker carries the check identity without changing what Bugbot is told.
5792
+ */
5793
+ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5794
+ /**
5795
+ * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
5796
+ * code block, say. Emitting it verbatim would split that check in two on the
5797
+ * next import, so `literal-` is inserted before `check:` on the way out and
5798
+ * taken off on the way back. `(?:literal-)*` makes it a ladder, so a body that
5799
+ * already contains an escaped marker survives the round trip too.
5800
+ */
5801
+ const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5802
+ const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5803
+ const FALLBACK_CHECK_NAME = "bugbot";
5804
+ function renderMarker(name) {
5805
+ return `<!-- rulesync:check:${name} -->`;
5806
+ }
5807
+ function escapeMarkers(content) {
5808
+ return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5809
+ }
5810
+ function unescapeMarkers(content) {
5811
+ return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5812
+ }
5813
+ function findMarkers(fileContent) {
5814
+ CHECK_MARKER_PATTERN.lastIndex = 0;
5815
+ const markers = [];
5816
+ let match = CHECK_MARKER_PATTERN.exec(fileContent);
5817
+ while (match !== null) {
5818
+ markers.push({
5819
+ name: match[1] ?? "",
5820
+ start: match.index,
5821
+ end: match.index + match[0].length
5822
+ });
5823
+ match = CHECK_MARKER_PATTERN.exec(fileContent);
5824
+ }
5825
+ return markers;
5826
+ }
5827
+ /**
5828
+ * The instruction text one check contributes. Bugbot has no field to put a
5829
+ * summary in, so `description` is used only when there is no body — the same
5830
+ * fallback the file-stem heading above it gets.
5831
+ */
5832
+ function toInstruction(rulesyncCheck) {
5833
+ const body = rulesyncCheck.getBody().trim();
5834
+ if (body.length > 0) return body;
5835
+ return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5836
+ }
5837
+ function renderSection(rulesyncCheck) {
5838
+ const name = (0, node_path.basename)(rulesyncCheck.getRelativeFilePath(), ".md");
5839
+ const heading = `## ${name}`;
5840
+ const instruction = toInstruction(rulesyncCheck);
5841
+ const lines = [renderMarker(name), heading];
5842
+ if (instruction.length > 0) lines.push("", escapeMarkers(instruction));
5843
+ return lines.join("\n");
5844
+ }
5845
+ /** Drop the heading generate writes, so a round trip does not stack headings. */
5846
+ function stripGeneratedHeading(section, name) {
5847
+ const [firstLine, ...rest] = section.split("\n");
5848
+ if (firstLine?.trim() === `## ${name}`) return rest.join("\n").trim();
5849
+ return section.trim();
5850
+ }
5851
+ /**
5852
+ * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5853
+ *
5854
+ * Bugbot takes one aggregated instruction file per directory rather than a file
5855
+ * per check, so every `.rulesync/checks/*.md` targeting Cursor collapses into
5856
+ * the repository-root `.cursor/BUGBOT.md` — hence {@link fromRulesyncChecks}
5857
+ * rather than the usual per-check conversion. Each check becomes one section:
5858
+ * an HTML-comment marker carrying the check name, an `## <name>` heading, and
5859
+ * the check body as the instruction text (the `description` is used when the
5860
+ * body is empty).
5861
+ *
5862
+ * Bugbot reads the file as free prose, so a check's `severity` and `tools` have
5863
+ * no equivalent there: they are not written and do not come back on import. So
5864
+ * is `description` whenever the check also has a body.
5865
+ *
5866
+ * Project scope only — Bugbot reads repository files, and there is no
5867
+ * user-level instruction file. Bugbot also merges nested `<dir>/.cursor/BUGBOT.md`
5868
+ * files found while traversing upward from changed files, but rulesync check
5869
+ * sources carry no directory-placement semantics, so only the root file is
5870
+ * generated.
5871
+ *
5872
+ * On import the markers split the file back into one check per section; content
5873
+ * before the first marker — and a hand-written file with no markers at all —
5874
+ * becomes a single `bugbot` check, so nothing in the file is dropped. A file
5875
+ * holding anything rulesync did not write is never deleted either (see
5876
+ * {@link canDeleteAuxiliaryFiles}), though generating checks for Cursor does
5877
+ * replace it — import first to keep what is there, which is warned about.
5878
+ *
5879
+ * @see https://cursor.com/docs/bugbot
5880
+ */
5881
+ var CursorCheck = class CursorCheck extends ToolCheck {
5882
+ static getSettablePaths(_options = {}) {
5883
+ return {
5884
+ relativeDirPath: CURSOR_DIR,
5885
+ relativeFilePath: CURSOR_BUGBOT_FILE_NAME
5886
+ };
5887
+ }
5888
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
5889
+ return this.isTargetedByRulesyncCheckDefault({
5890
+ rulesyncCheck,
5891
+ toolTarget: "cursor"
5892
+ });
5893
+ }
5894
+ /**
5895
+ * Ownership guard the processor consults before it deletes anything for this
5896
+ * tool. `.cursor/BUGBOT.md` is a file Cursor's own documentation tells users
5897
+ * to hand-write, so anything in it that rulesync did not write is not
5898
+ * rulesync's to remove — dropping the last check targeting Cursor must not
5899
+ * take somebody's hand-written review instructions with it. Deletion is
5900
+ * therefore allowed only for a file that is nothing but generated sections:
5901
+ * one that carries no marker at all, or that carries hand-written text ahead
5902
+ * of the first marker, stays.
5903
+ */
5904
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
5905
+ const paths = CursorCheck.getSettablePaths();
5906
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
5907
+ if (fileContent === null) return true;
5908
+ const firstMarkerStart = findMarkers(fileContent)[0]?.start;
5909
+ if (firstMarkerStart === void 0) return false;
5910
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5911
+ }
5912
+ static fromRulesyncCheck(_params) {
5913
+ throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
5914
+ }
5915
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
5916
+ if (rulesyncChecks.length === 0) return [];
5917
+ const paths = CursorCheck.getSettablePaths({ global });
5918
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5919
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
5920
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
5921
+ const firstMarkerStart = findMarkers(existingContent)[0]?.start ?? existingContent.length;
5922
+ if (existingContent.slice(0, firstMarkerStart).trim().length > 0) logger?.warn(`Cursor checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets cursor --features checks\` first to keep them.`);
5923
+ const fileContent = `${rulesyncChecks.map(renderSection).join("\n\n")}\n`;
5924
+ return [new CursorCheck({
5925
+ outputRoot,
5926
+ relativeDirPath: paths.relativeDirPath,
5927
+ relativeFilePath,
5928
+ fileContent,
5929
+ global
5930
+ })];
5931
+ }
5932
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
5933
+ const paths = CursorCheck.getSettablePaths({ global });
5934
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5935
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
5936
+ return new CursorCheck({
5937
+ outputRoot,
5938
+ relativeDirPath: paths.relativeDirPath,
5939
+ relativeFilePath,
5940
+ fileContent: await readFileContentOrNull(filePath) ?? "",
5941
+ global
5942
+ });
5943
+ }
5944
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
5945
+ return new CursorCheck({
5946
+ outputRoot,
5947
+ relativeDirPath,
5948
+ relativeFilePath,
5949
+ fileContent: "",
5950
+ validate: false,
5951
+ global
5952
+ });
5953
+ }
5954
+ validate() {
5955
+ return {
5956
+ success: true,
5957
+ error: null
5958
+ };
5959
+ }
5960
+ toRulesyncCheck() {
5961
+ const first = this.toRulesyncChecks()[0];
5962
+ if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
5963
+ return first;
5964
+ }
5965
+ toRulesyncChecks() {
5966
+ const fileContent = this.getFileContent();
5967
+ const sections = [];
5968
+ const markers = findMarkers(fileContent);
5969
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5970
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5971
+ if (preamble.length > 0) sections.push({
5972
+ name: FALLBACK_CHECK_NAME,
5973
+ content: unescapeMarkers(preamble)
5974
+ });
5975
+ for (const [index, marker] of markers.entries()) {
5976
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5977
+ const markerName = marker.name.trim();
5978
+ const name = slugifyCheckName(markerName) || FALLBACK_CHECK_NAME;
5979
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5980
+ sections.push({
5981
+ name,
5982
+ content: unescapeMarkers(content)
5983
+ });
5984
+ }
5985
+ const used = /* @__PURE__ */ new Set();
5986
+ return sections.map(({ name, content }) => {
5987
+ let uniqueName = name;
5988
+ let suffix = 2;
5989
+ while (used.has(uniqueName)) {
5990
+ uniqueName = `${name}-${suffix}`;
5991
+ suffix += 1;
5992
+ }
5993
+ used.add(uniqueName);
5994
+ return new RulesyncCheck({
5995
+ outputRoot: ".",
5996
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5997
+ relativeFilePath: `${uniqueName}.md`,
5998
+ frontmatter: { targets: ["*"] },
5999
+ body: content
6000
+ });
6001
+ });
6002
+ }
6003
+ };
6004
+ //#endregion
5677
6005
  //#region src/constants/hermesagent-paths.ts
5678
6006
  /**
5679
6007
  * Hermes Agent configuration-layout conventions.
@@ -6172,7 +6500,20 @@ const SHARED_CONFIG_OWNERSHIP = {
6172
6500
  jsoncParseErrors: "error",
6173
6501
  features: { permissions: {
6174
6502
  kind: "replace-owned-keys",
6175
- ownedKeys: ["chat.tools.terminal.autoApprove"]
6503
+ ownedKeys: [
6504
+ "chat.tools.terminal.autoApprove",
6505
+ "chat.tools.edits.autoApprove",
6506
+ "chat.tools.urls.autoApprove"
6507
+ ]
6508
+ } }
6509
+ },
6510
+ ".vscode/mcp.json": {
6511
+ format: "jsonc",
6512
+ invalidRootPolicy: "error",
6513
+ jsoncParseErrors: "error",
6514
+ features: { mcp: {
6515
+ kind: "replace-owned-keys",
6516
+ ownedKeys: ["servers"]
6176
6517
  } }
6177
6518
  },
6178
6519
  ".qwen/settings.json": {
@@ -6538,7 +6879,12 @@ const applyIgnoreReadDenies = (params) => {
6538
6879
  const applyPermissions = (params) => {
6539
6880
  const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
6540
6881
  const current = parsePermissionsBlock(settings);
6541
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
6882
+ const emitted = /* @__PURE__ */ new Set([
6883
+ ...allow,
6884
+ ...ask,
6885
+ ...deny
6886
+ ]);
6887
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)) && !emitted.has(entry));
6542
6888
  if (logger && managedToolNames.has(READ_TOOL_NAME)) {
6543
6889
  const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
6544
6890
  if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
@@ -6688,12 +7034,9 @@ function toRulesyncCheckFromGate({ gate, index, scope, editOnly }) {
6688
7034
  body: ""
6689
7035
  });
6690
7036
  }
6691
- function slugify(value) {
6692
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
6693
- }
6694
7037
  function slugForGate({ gate, index, scope }) {
6695
- const slug = slugify(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
6696
- return `${scope ? `${slugify(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
7038
+ const slug = slugifyCheckName(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
7039
+ return `${scope ? `${slugifyCheckName(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
6697
7040
  }
6698
7041
  /**
6699
7042
  * Checks adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -6869,6 +7212,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
6869
7212
  filePattern: "*.md"
6870
7213
  }
6871
7214
  }],
7215
+ ["cursor", {
7216
+ class: CursorCheck,
7217
+ meta: {
7218
+ supportsGlobal: false,
7219
+ filePattern: CURSOR_BUGBOT_FILE_NAME
7220
+ }
7221
+ }],
6872
7222
  ["hermesagent", {
6873
7223
  class: HermesagentCheck,
6874
7224
  meta: {
@@ -7284,6 +7634,7 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
7284
7634
  const ANTIGRAVITY_DIR = ".agents";
7285
7635
  const ANTIGRAVITY_SKILLS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "skills");
7286
7636
  const ANTIGRAVITY_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "workflows");
7637
+ const ANTIGRAVITY_AGENTS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "agents");
7287
7638
  const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
7288
7639
  const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
7289
7640
  const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
@@ -7294,6 +7645,7 @@ const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
7294
7645
  const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
7295
7646
  const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
7296
7647
  const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
7648
+ const ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH, "agents");
7297
7649
  //#endregion
7298
7650
  //#region src/constants/antigravity-cli-paths.ts
7299
7651
  const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
@@ -7852,6 +8204,7 @@ const CLAUDECODE_PLUGIN_HOOKS_DIR = "hooks";
7852
8204
  const CLAUDECODE_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7853
8205
  const ANTIGRAVITY_PLUGIN_RULES_DIR = "rules";
7854
8206
  const ANTIGRAVITY_PLUGIN_SKILLS_DIR = "skills";
8207
+ const ANTIGRAVITY_PLUGIN_AGENTS_DIR = "agents";
7855
8208
  const ANTIGRAVITY_PLUGIN_MCP_FILE_NAME = "mcp_config.json";
7856
8209
  const ANTIGRAVITY_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7857
8210
  //#endregion
@@ -8076,6 +8429,7 @@ const COPILOT_SKILLS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "skills");
8076
8429
  const COPILOT_AGENTS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "agents");
8077
8430
  const COPILOT_HOOKS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "hooks");
8078
8431
  const COPILOT_HOOKS_FILE_NAME = "copilot-hooks.json";
8432
+ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
8079
8433
  const COPILOT_MCP_DIR = ".vscode";
8080
8434
  const COPILOT_MCP_FILE_NAME = "mcp.json";
8081
8435
  const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
@@ -8083,6 +8437,7 @@ const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
8083
8437
  const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
8084
8438
  const COPILOTCLI_AGENTS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "agents");
8085
8439
  const COPILOTCLI_HOOKS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "hooks");
8440
+ const COPILOT_GLOBAL_HOOKS_DIR_PATH = COPILOTCLI_HOOKS_DIR_PATH;
8086
8441
  const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
8087
8442
  const COPILOT_SKILLS_GLOBAL_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "skills");
8088
8443
  //#endregion
@@ -8206,17 +8561,6 @@ var CopilotCommand = class CopilotCommand extends ToolCommand {
8206
8561
  }
8207
8562
  };
8208
8563
  //#endregion
8209
- //#region src/constants/cursor-paths.ts
8210
- const CURSOR_DIR = ".cursor";
8211
- const CURSOR_COMMANDS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "commands");
8212
- const CURSOR_SKILLS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "skills");
8213
- const CURSOR_AGENTS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "agents");
8214
- const CURSOR_MCP_FILE_NAME = "mcp.json";
8215
- const CURSOR_HOOKS_FILE_NAME = "hooks.json";
8216
- const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
8217
- const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
8218
- const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
8219
- //#endregion
8220
8564
  //#region src/features/commands/cursor-command.ts
8221
8565
  const CursorCommandFrontmatterSchema = zod_mini.z.looseObject({
8222
8566
  description: zod_mini.z.optional(zod_mini.z.string()),
@@ -11985,6 +12329,13 @@ function groupDefinitionsByMatcher(definitions) {
11985
12329
  }
11986
12330
  return byMatcher;
11987
12331
  }
12332
+ /** `$CLAUDE_PROJECT_DIR` -> `${CLAUDE_PROJECT_DIR}`, the form the tool substitutes. */
12333
+ function bracePlaceholder(projectDirVar) {
12334
+ return `\${${projectDirVar.replace(/^\$/, "")}}`;
12335
+ }
12336
+ function stripSurroundingQuotes(value) {
12337
+ return value.replace(/^(["'])(.*)\1$/, "$2").replace(/^["']/, "");
12338
+ }
11988
12339
  /**
11989
12340
  * Apply the optional project directory variable prefix to a command string.
11990
12341
  */
@@ -11994,8 +12345,10 @@ function applyCommandPrefix({ def, converterConfig }) {
11994
12345
  const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
11995
12346
  const isDotRelativeCommand = unquotedCommand?.startsWith(".") ?? false;
11996
12347
  const isAbsoluteCommand = typeof unquotedCommand === "string" && (node_path.posix.isAbsolute(unquotedCommand) || node_path.win32.isAbsolute(unquotedCommand) || unquotedCommand.startsWith("~/"));
12348
+ const isExecForm = (converterConfig.arrayPassthroughFields?.some(({ canonical }) => canonical === "args") ?? false) && Array.isArray(def.args);
11997
12349
  if (!(converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && !isAbsoluteCommand && (!converterConfig.prefixDotRelativeCommandsOnly || isDotRelativeCommand)) || typeof trimmedCommand !== "string") return def.command;
11998
12350
  const relativeCommand = trimmedCommand.replace(/^(["'])\.\//, "$1").replace(/^\.\//, "");
12351
+ if (isExecForm) return `${bracePlaceholder(converterConfig.projectDirVar)}/${stripSurroundingQuotes(relativeCommand)}`;
11999
12352
  return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
12000
12353
  }
12001
12354
  /**
@@ -12003,8 +12356,11 @@ function applyCommandPrefix({ def, converterConfig }) {
12003
12356
  * canonical field name to its (possibly renamed) tool field name. Only boolean
12004
12357
  * values are carried through.
12005
12358
  */
12006
- function emitBooleanPassthroughFields({ def, converterConfig }) {
12007
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "boolean").map(({ canonical, tool }) => [tool, def[canonical]]));
12359
+ function emitBooleanPassthroughFields({ def, hookType, converterConfig }) {
12360
+ return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12361
+ if (commandOnly === true && hookType !== "command") return false;
12362
+ return typeof def[canonical] === "boolean";
12363
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12008
12364
  }
12009
12365
  /**
12010
12366
  * Import the configured boolean passthrough fields back into canonical fields,
@@ -12018,8 +12374,11 @@ function importBooleanPassthroughFields({ h, converterConfig }) {
12018
12374
  * canonical field name to its (possibly renamed) tool field name. Only non-empty
12019
12375
  * string values are carried through.
12020
12376
  */
12021
- function emitStringPassthroughFields({ def, converterConfig }) {
12022
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "string" && def[canonical] !== "").map(({ canonical, tool }) => [tool, def[canonical]]));
12377
+ function emitStringPassthroughFields({ def, hookType, converterConfig }) {
12378
+ return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12379
+ if (commandOnly === true && hookType !== "command") return false;
12380
+ return typeof def[canonical] === "string" && def[canonical] !== "";
12381
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12023
12382
  }
12024
12383
  /**
12025
12384
  * Import the configured string passthrough fields back into canonical fields,
@@ -12032,8 +12391,11 @@ function importStringPassthroughFields({ h, converterConfig }) {
12032
12391
  /**
12033
12392
  * Emit the configured string-array passthrough fields on the tool side.
12034
12393
  */
12035
- function emitArrayPassthroughFields({ def, converterConfig }) {
12036
- return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical }) => isStringArray(def[canonical])).map(({ canonical, tool }) => [tool, def[canonical]]));
12394
+ function emitArrayPassthroughFields({ def, hookType, converterConfig }) {
12395
+ return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12396
+ if (commandOnly === true && hookType !== "command") return false;
12397
+ return isStringArray(def[canonical]);
12398
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12037
12399
  }
12038
12400
  /**
12039
12401
  * Import the configured string-array passthrough fields, reversing
@@ -12110,14 +12472,17 @@ function buildToolHooks({ defs, converterConfig }) {
12110
12472
  hooks.push({
12111
12473
  ...emitBooleanPassthroughFields({
12112
12474
  def,
12475
+ hookType,
12113
12476
  converterConfig
12114
12477
  }),
12115
12478
  ...emitStringPassthroughFields({
12116
12479
  def,
12480
+ hookType,
12117
12481
  converterConfig
12118
12482
  }),
12119
12483
  ...emitArrayPassthroughFields({
12120
12484
  def,
12485
+ hookType,
12121
12486
  converterConfig
12122
12487
  }),
12123
12488
  type: hookType,
@@ -12201,6 +12566,8 @@ function stripCommandPrefix({ command, converterConfig }) {
12201
12566
  if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
12202
12567
  const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
12203
12568
  if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
12569
+ const bracedPrefix = `${bracePlaceholder(converterConfig.projectDirVar)}/`;
12570
+ if (cmd.startsWith(bracedPrefix)) return `./${cmd.slice(bracedPrefix.length)}`;
12204
12571
  if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
12205
12572
  const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12206
12573
  return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
@@ -12748,7 +13115,10 @@ const CLAUDE_CONVERTER_CONFIG = {
12748
13115
  "taskCreated",
12749
13116
  "taskCompleted",
12750
13117
  "teammateIdle",
12751
- "cwdChanged"
13118
+ "cwdChanged",
13119
+ "beforeSubmitPrompt",
13120
+ "stop",
13121
+ "directoryAdded"
12752
13122
  ]),
12753
13123
  supportedHookTypes: /* @__PURE__ */ new Set([
12754
13124
  "command",
@@ -12758,9 +13128,45 @@ const CLAUDE_CONVERTER_CONFIG = {
12758
13128
  "agent"
12759
13129
  ]),
12760
13130
  emitsPromptModel: true,
12761
- stringPassthroughFields: [{
12762
- canonical: "if",
12763
- tool: "if"
13131
+ stringPassthroughFields: [
13132
+ {
13133
+ canonical: "if",
13134
+ tool: "if"
13135
+ },
13136
+ {
13137
+ canonical: "statusMessage",
13138
+ tool: "statusMessage"
13139
+ },
13140
+ {
13141
+ canonical: "shell",
13142
+ tool: "shell",
13143
+ commandOnly: true
13144
+ }
13145
+ ],
13146
+ booleanPassthroughFields: [
13147
+ {
13148
+ canonical: "once",
13149
+ tool: "once"
13150
+ },
13151
+ {
13152
+ canonical: "async",
13153
+ tool: "async",
13154
+ commandOnly: true
13155
+ },
13156
+ {
13157
+ canonical: "asyncRewake",
13158
+ tool: "asyncRewake",
13159
+ commandOnly: true
13160
+ },
13161
+ {
13162
+ canonical: "continueOnBlock",
13163
+ tool: "continueOnBlock"
13164
+ }
13165
+ ],
13166
+ arrayPassthroughFields: [{
13167
+ canonical: "args",
13168
+ tool: "args",
13169
+ commandOnly: true
12764
13170
  }]
12765
13171
  };
12766
13172
  var ClaudecodeHooks = class extends ToolHooks {
@@ -12868,7 +13274,14 @@ const CODEXCLI_CONVERTER_CONFIG = {
12868
13274
  toolToCanonicalEventNames: CODEXCLI_TO_CANONICAL_EVENT_NAMES,
12869
13275
  projectDirVar: "",
12870
13276
  supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
12871
- passthroughFields: ["name", "description"]
13277
+ passthroughFields: ["name", "description"],
13278
+ stringPassthroughFields: [{
13279
+ canonical: "commandWindows",
13280
+ tool: "commandWindows"
13281
+ }, {
13282
+ canonical: "statusMessage",
13283
+ tool: "statusMessage"
13284
+ }]
12872
13285
  };
12873
13286
  /**
12874
13287
  * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
@@ -13116,7 +13529,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13116
13529
  fileContent: params.fileContent ?? "{}"
13117
13530
  });
13118
13531
  }
13119
- static getSettablePaths(_options = {}) {
13532
+ static getSettablePaths({ global = false } = {}) {
13533
+ if (global) return {
13534
+ relativeDirPath: COPILOT_GLOBAL_HOOKS_DIR_PATH,
13535
+ relativeFilePath: COPILOT_GLOBAL_HOOKS_FILE_NAME
13536
+ };
13120
13537
  return {
13121
13538
  relativeDirPath: COPILOT_HOOKS_DIR_PATH,
13122
13539
  relativeFilePath: COPILOT_HOOKS_FILE_NAME
@@ -13130,11 +13547,12 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13130
13547
  relativeDirPath: paths.relativeDirPath,
13131
13548
  relativeFilePath: paths.relativeFilePath,
13132
13549
  fileContent,
13133
- validate
13550
+ validate,
13551
+ global
13134
13552
  });
13135
13553
  }
13136
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true }) {
13137
- const paths = CopilotHooks.getSettablePaths();
13554
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
13555
+ const paths = CopilotHooks.getSettablePaths({ global });
13138
13556
  const copilotHooks = canonicalToCopilotHooks(rulesyncHooks.getJson());
13139
13557
  const fileContent = JSON.stringify({
13140
13558
  version: 1,
@@ -13145,7 +13563,8 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13145
13563
  relativeDirPath: paths.relativeDirPath,
13146
13564
  relativeFilePath: paths.relativeFilePath,
13147
13565
  fileContent,
13148
- validate
13566
+ validate,
13567
+ global
13149
13568
  });
13150
13569
  }
13151
13570
  toRulesyncHooks(options) {
@@ -13189,10 +13608,16 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13189
13608
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
13190
13609
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
13191
13610
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
13192
- * `notification`, `preMcpToolCall`). Each entry supports three hook types:
13611
+ * `notification`, `userPromptTransformed`, `preMcpToolCall`). Each entry
13612
+ * supports three hook types:
13193
13613
  *
13194
13614
  * - `command` — the `bash` / `powershell` command-field shape with optional
13195
- * `timeoutSec`, plus optional `cwd` / `env`.
13615
+ * `timeoutSec`, plus optional `cwd` / `env`. Upstream also accepts the
13616
+ * portable `command` field (copied to both shells when neither is present)
13617
+ * and `timeout` as an alias for `timeoutSec`; rulesync reads both on import.
13618
+ * On generate the canonical `shell` selector picks `bash` or `powershell`,
13619
+ * and without it the portable `command` field is written — so the generated
13620
+ * file does not depend on the machine rulesync ran on.
13196
13621
  * - `prompt` — a `prompt` string (Copilot CLI only honors prompt hooks on
13197
13622
  * `sessionStart`, so prompt hooks on other events are skipped).
13198
13623
  * - `http` — `url` / `headers` / `allowedEnvVars` with optional `timeoutSec`.
@@ -13218,11 +13643,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13218
13643
  * all rulesync-managed Copilot CLI files under the single `~/.copilot/`
13219
13644
  * root and will revisit if the spec later mandates an alternate layout.
13220
13645
  *
13221
- * Hook entries on `preToolUse` / `postToolUse` may carry an optional `matcher`
13222
- * field (a regex compiled as `^(?:PATTERN)$`, tested against `toolName`); it is
13223
- * emitted on those events and dropped (with a warning) on any other event,
13224
- * which never honors matchers. See changelog v1.0.36 (2026-04-24) and v1.0.63
13225
- * (2026-06-15). Reference: https://docs.github.com/en/copilot/reference/hooks-reference
13646
+ * Hook entries on the six matcher-aware events (see
13647
+ * {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
13648
+ * is emitted on those events and dropped (with a warning) on any other event,
13649
+ * which never honors matchers.
13650
+ * Reference: https://docs.github.com/en/copilot/reference/hooks-reference
13226
13651
  *
13227
13652
  * The output JSON schema and platform-specific `bash` / `powershell` command
13228
13653
  * field selection match `copilot-hooks.ts`, but the event surface diverges (the
@@ -13237,19 +13662,30 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13237
13662
  * `Edit|Write`) are now honored instead of silently dropped").
13238
13663
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
13239
13664
  */
13240
- const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
13665
+ const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set([
13666
+ "preToolUse",
13667
+ "postToolUse",
13668
+ "notification",
13669
+ "permissionRequest",
13670
+ "preCompact",
13671
+ "subagentStart"
13672
+ ]);
13673
+ /** Human-readable list of the matcher-aware events, for the drop warning. */
13674
+ const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
13241
13675
  const CopilotCliHookEntrySchema = zod_mini.z.looseObject({
13242
13676
  type: zod_mini.z._default(zod_mini.z.string(), "command"),
13243
13677
  matcher: zod_mini.z.optional(zod_mini.z.string()),
13244
13678
  bash: zod_mini.z.optional(zod_mini.z.string()),
13245
13679
  powershell: zod_mini.z.optional(zod_mini.z.string()),
13680
+ command: zod_mini.z.optional(zod_mini.z.string()),
13246
13681
  prompt: zod_mini.z.optional(zod_mini.z.string()),
13247
13682
  url: zod_mini.z.optional(zod_mini.z.string()),
13248
13683
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
13249
13684
  allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
13250
13685
  cwd: zod_mini.z.optional(zod_mini.z.string()),
13251
13686
  env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
13252
- timeoutSec: zod_mini.z.optional(zod_mini.z.number())
13687
+ timeoutSec: zod_mini.z.optional(zod_mini.z.number()),
13688
+ timeout: zod_mini.z.optional(zod_mini.z.number())
13253
13689
  });
13254
13690
  /** Filter the shared config hooks down to events the Copilot CLI supports. */
13255
13691
  function filterSupportedCopilotCliHooks(hooks) {
@@ -13260,21 +13696,21 @@ function filterSupportedCopilotCliHooks(hooks) {
13260
13696
  }
13261
13697
  /**
13262
13698
  * Resolve the `matcher` part for an exported entry. Copilot CLI honors `matcher`
13263
- * only on preToolUse/postToolUse entries; on any other event a matcher would be
13264
- * silently dropped by the CLI, so we drop it here with a warning rather than
13265
- * emitting a dead field.
13699
+ * only on the events listed in {@link COPILOTCLI_MATCHER_EVENTS}; on any other
13700
+ * event a matcher would be silently dropped by the CLI, so we drop it here with
13701
+ * a warning rather than emitting a dead field.
13266
13702
  */
13267
13703
  function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger }) {
13268
13704
  if (matcher === void 0 || matcher === null || matcher === "") return {};
13269
13705
  if (matcherSupported) return { matcher };
13270
- logger?.warn(`Copilot CLI hook matchers are only honored on preToolUse/postToolUse; dropping matcher "${matcher}" on '${eventName}'.`);
13706
+ logger?.warn(`Copilot CLI hook matchers are only honored on ${COPILOTCLI_MATCHER_EVENTS_LABEL}; dropping matcher "${matcher}" on '${eventName}'.`);
13271
13707
  return {};
13272
13708
  }
13273
13709
  /**
13274
13710
  * Build the exported entries for a single canonical event. Returns an empty
13275
13711
  * array when no entries are emitted (e.g. all prompt hooks were skipped).
13276
13712
  */
13277
- function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, commandField, logger }) {
13713
+ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, logger }) {
13278
13714
  const matcherSupported = COPILOTCLI_MATCHER_EVENTS.has(eventName);
13279
13715
  const entries = [];
13280
13716
  for (const def of definitions) {
@@ -13310,22 +13746,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
13310
13746
  ...timeoutPart,
13311
13747
  ...rest
13312
13748
  });
13313
- else if (hookType === "command") entries.push({
13314
- type: "command",
13315
- ...matcherPart,
13316
- ...compact({
13317
- [commandField]: def.command,
13318
- env: def.env
13319
- }),
13320
- ...timeoutPart,
13321
- ...rest
13322
- });
13749
+ else if (hookType === "command") {
13750
+ const commandField = def.shell ?? "command";
13751
+ entries.push({
13752
+ type: "command",
13753
+ ...matcherPart,
13754
+ ...compact({
13755
+ [commandField]: def.command,
13756
+ env: def.env
13757
+ }),
13758
+ ...timeoutPart,
13759
+ ...rest
13760
+ });
13761
+ }
13323
13762
  }
13324
13763
  return entries;
13325
13764
  }
13326
13765
  function canonicalToCopilotCliHooks(config, logger) {
13327
13766
  const canonicalSchemaKeys = Object.keys(HookDefinitionSchema.shape);
13328
- const commandField = process.platform === "win32" ? "powershell" : "bash";
13329
13767
  const effectiveHooks = {
13330
13768
  ...filterSupportedCopilotCliHooks(config.hooks),
13331
13769
  ...config.copilot?.hooks,
@@ -13338,7 +13776,6 @@ function canonicalToCopilotCliHooks(config, logger) {
13338
13776
  eventName,
13339
13777
  definitions,
13340
13778
  canonicalSchemaKeys,
13341
- commandField,
13342
13779
  logger
13343
13780
  });
13344
13781
  if (entries.length > 0) out[copilotEventName] = entries;
@@ -13355,6 +13792,13 @@ function importPassthrough(entry) {
13355
13792
  if (entry.env !== void 0) passthrough.env = entry.env;
13356
13793
  return passthrough;
13357
13794
  }
13795
+ /**
13796
+ * Resolve the canonical command and its `shell` selector from an imported entry.
13797
+ *
13798
+ * A shell-specific field carries its `shell` through so re-export writes the
13799
+ * same field back. An entry using only the portable `command` field leaves
13800
+ * `shell` unset, which re-export renders as the portable field again.
13801
+ */
13358
13802
  function resolveImportCommand(entry, logger) {
13359
13803
  const hasBash = typeof entry.bash === "string";
13360
13804
  const hasPowershell = typeof entry.powershell === "string";
@@ -13363,9 +13807,22 @@ function resolveImportCommand(entry, logger) {
13363
13807
  const chosen = isWindows ? "powershell" : "bash";
13364
13808
  const ignored = isWindows ? "bash" : "powershell";
13365
13809
  logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
13366
- return isWindows ? entry.powershell : entry.bash;
13367
- } else if (hasBash) return entry.bash;
13368
- else if (hasPowershell) return entry.powershell;
13810
+ return isWindows ? {
13811
+ command: entry.powershell,
13812
+ shell: "powershell"
13813
+ } : {
13814
+ command: entry.bash,
13815
+ shell: "bash"
13816
+ };
13817
+ } else if (hasBash) return {
13818
+ command: entry.bash,
13819
+ shell: "bash"
13820
+ };
13821
+ else if (hasPowershell) return {
13822
+ command: entry.powershell,
13823
+ shell: "powershell"
13824
+ };
13825
+ return typeof entry.command === "string" ? { command: entry.command } : {};
13369
13826
  }
13370
13827
  function copilotCliHooksToCanonical(rawHooks, logger) {
13371
13828
  if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
@@ -13378,7 +13835,7 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13378
13835
  const parseResult = CopilotCliHookEntrySchema.safeParse(rawEntry);
13379
13836
  if (!parseResult.success) continue;
13380
13837
  const entry = parseResult.data;
13381
- const timeout = entry.timeoutSec;
13838
+ const timeout = entry.timeoutSec ?? entry.timeout;
13382
13839
  const timeoutPart = timeout !== void 0 ? { timeout } : {};
13383
13840
  const matcherPart = entry.matcher !== void 0 && entry.matcher !== "" ? { matcher: entry.matcher } : {};
13384
13841
  const passthrough = importPassthrough(entry);
@@ -13397,10 +13854,11 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13397
13854
  ...passthrough
13398
13855
  });
13399
13856
  else {
13400
- const command = resolveImportCommand(entry, logger);
13857
+ const { command, shell } = resolveImportCommand(entry, logger);
13401
13858
  defs.push({
13402
13859
  type: "command",
13403
13860
  ...command !== void 0 && { command },
13861
+ ...shell !== void 0 && { shell },
13404
13862
  ...matcherPart,
13405
13863
  ...timeoutPart,
13406
13864
  ...passthrough
@@ -14540,7 +14998,23 @@ var JunieHooks = class JunieHooks extends ToolHooks {
14540
14998
  };
14541
14999
  //#endregion
14542
15000
  //#region src/features/hooks/opencode-style-generator.ts
14543
- const NAMED_HOOKS = /* @__PURE__ */ new Set(["tool.execute.before", "tool.execute.after"]);
15001
+ /**
15002
+ * Tool events emitted as named `(input, ...)` hooks rather than through the
15003
+ * generic `event.type` dispatch, mapped to the expression a hook's `matcher`
15004
+ * regex is tested against — or `null` when the hook has no matchable subject,
15005
+ * in which case a matcher is dropped rather than compiled against a field that
15006
+ * does not exist.
15007
+ *
15008
+ * `experimental.session.compacting` receives `(input, output)` and exposes no
15009
+ * per-invocation identifier worth matching on, so it takes `null`.
15010
+ *
15011
+ * @see https://opencode.ai/docs/plugins/
15012
+ */
15013
+ const NAMED_HOOK_MATCHER_SUBJECTS = {
15014
+ "tool.execute.before": "input.tool",
15015
+ "tool.execute.after": "input.tool",
15016
+ "experimental.session.compacting": null
15017
+ };
14544
15018
  function escapeForTemplateLiteral(command) {
14545
15019
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
14546
15020
  }
@@ -14573,7 +15047,7 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
14573
15047
  });
14574
15048
  }
14575
15049
  if (handlers.length > 0) {
14576
- const grouped = NAMED_HOOKS.has(toolEvent) ? namedEventHandlers : genericEventHandlers;
15050
+ const grouped = Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) ? namedEventHandlers : genericEventHandlers;
14577
15051
  const existing = grouped[toolEvent];
14578
15052
  if (existing) existing.push(...handlers);
14579
15053
  else grouped[toolEvent] = handlers;
@@ -14602,14 +15076,15 @@ function buildGenericEventBodyLines(genericEventHandlers) {
14602
15076
  function buildNamedEventBodyLines(namedEventHandlers) {
14603
15077
  const bodyLines = [];
14604
15078
  for (const [eventName, handlers] of Object.entries(namedEventHandlers)) {
15079
+ const matcherSubject = NAMED_HOOK_MATCHER_SUBJECTS[eventName] ?? null;
14605
15080
  bodyLines.push(` "${eventName}": async (input) => {`);
14606
15081
  for (const handler of handlers) {
14607
15082
  const escapedCommand = escapeForTemplateLiteral(handler.command);
14608
- if (handler.matcher) {
15083
+ if (handler.matcher && matcherSubject !== null) {
14609
15084
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
14610
15085
  bodyLines.push(" {");
14611
15086
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
14612
- bodyLines.push(` if (__re.test(input.tool)) {`);
15087
+ bodyLines.push(` if (__re.test(${matcherSubject})) {`);
14613
15088
  bodyLines.push(` await $\`${escapedCommand}\`;`);
14614
15089
  bodyLines.push(" }");
14615
15090
  bodyLines.push(" }");
@@ -16245,7 +16720,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
16245
16720
  class: CopilotHooks,
16246
16721
  meta: {
16247
16722
  supportsProject: true,
16248
- supportsGlobal: false,
16723
+ supportsGlobal: true,
16249
16724
  supportsImport: true
16250
16725
  },
16251
16726
  supportedEvents: COPILOT_HOOK_EVENTS,
@@ -18722,8 +19197,20 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
18722
19197
  disabledTools: "disabled_tools",
18723
19198
  envVars: "env_vars"
18724
19199
  };
19200
+ const RULESYNC_TO_CODEX_SCALAR_FIELD_MAP = { experimentalEnvironment: "experimental_environment" };
19201
+ const CODEX_TO_RULESYNC_SCALAR_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
18725
19202
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
18726
19203
  /**
19204
+ * `env_vars` entries are either a bare variable name or `{ name, source }`,
19205
+ * where `source = "remote"` reads the variable from the remote executor
19206
+ * environment. The other renamed keys (`enabled_tools`, `disabled_tools`) stay
19207
+ * plain string arrays, so the widened check applies to `env_vars` only.
19208
+ * @see https://learn.chatgpt.com/docs/extend/mcp
19209
+ */
19210
+ function isValidRenamedArray(key, value) {
19211
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
19212
+ }
19213
+ /**
18727
19214
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
18728
19215
  * Code style camelCase) into the shape Codex CLI understands. Codex expects the
18729
19216
  * OAuth client id under snake_case `client_id`; without it `codex mcp login`
@@ -18783,8 +19270,12 @@ function convertFromCodexFormat(codexMcp) {
18783
19270
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
18784
19271
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
18785
19272
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
18786
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
19273
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18787
19274
  else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
19275
+ } else if (Object.hasOwn(CODEX_TO_RULESYNC_SCALAR_FIELD_MAP, key)) {
19276
+ const mappedKey = CODEX_TO_RULESYNC_SCALAR_FIELD_MAP[key];
19277
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
19278
+ else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
18788
19279
  } else converted[key] = value;
18789
19280
  }
18790
19281
  result[name] = converted;
@@ -18806,8 +19297,12 @@ function convertToCodexFormat(mcpServers) {
18806
19297
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
18807
19298
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
18808
19299
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
18809
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
19300
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18810
19301
  else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
19302
+ } else if (Object.hasOwn(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP, key)) {
19303
+ const mappedKey = RULESYNC_TO_CODEX_SCALAR_FIELD_MAP[key];
19304
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
19305
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
18811
19306
  } else converted[key] = value;
18812
19307
  }
18813
19308
  const previousName = originalNames.get(codexName);
@@ -18878,7 +19373,9 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18878
19373
  const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
18879
19374
  return [serverName, {
18880
19375
  ...serverConfig,
18881
- ...isRecord(rawServer) && isStringArray$1(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
19376
+ ...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
19377
+ ...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
19378
+ ...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
18882
19379
  }];
18883
19380
  })));
18884
19381
  const filteredMcpServers = this.removeEmptyEntries(converted);
@@ -18949,9 +19446,6 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18949
19446
  };
18950
19447
  //#endregion
18951
19448
  //#region src/features/mcp/copilot-mcp.ts
18952
- function convertToCopilotFormat(mcpServers) {
18953
- return { servers: mcpServers };
18954
- }
18955
19449
  function convertFromCopilotFormat(copilotConfig) {
18956
19450
  return copilotConfig.servers ?? {};
18957
19451
  }
@@ -18980,13 +19474,21 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
18980
19474
  validate
18981
19475
  });
18982
19476
  }
18983
- static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
18984
- const copilotConfig = convertToCopilotFormat(rulesyncMcp.getMcpServers());
19477
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
19478
+ const paths = this.getSettablePaths();
19479
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
19480
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
18985
19481
  return new CopilotMcp({
18986
19482
  outputRoot,
18987
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18988
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18989
- fileContent: JSON.stringify(copilotConfig, null, 2),
19483
+ relativeDirPath: paths.relativeDirPath,
19484
+ relativeFilePath: paths.relativeFilePath,
19485
+ fileContent: applySharedConfigPatch({
19486
+ fileKey: sharedConfigFileKey(paths),
19487
+ feature: "mcp",
19488
+ existingContent,
19489
+ patch: { servers: rulesyncMcp.getMcpServers() },
19490
+ filePath
19491
+ }),
18990
19492
  validate
18991
19493
  });
18992
19494
  }
@@ -20669,7 +21171,7 @@ const KILO_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
20669
21171
  * impossible to start — no command, no URL — so it is written as the toggle it
20670
21172
  * resembles rather than dropped, but the loss is said out loud.
20671
21173
  */
20672
- function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
21174
+ function warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger) {
20673
21175
  const dropped = Object.keys(serverConfig).filter((key) => !KILO_TOGGLE_KEPT_KEYS.has(key));
20674
21176
  if (dropped.length === 0) return;
20675
21177
  logger?.warn(`Kilo MCP: "${serverName}" declares no transport, so it is written as a toggle entry and ${dropped.toSorted().join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
@@ -20682,7 +21184,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20682
21184
  if (declaresNoTransport(serverConfig)) {
20683
21185
  if (serverConfig.disabled === void 0) {
20684
21186
  if (existingEntry !== void 0 && !isKiloTransportServer(existingEntry)) {
20685
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21187
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20686
21188
  return existingEntry;
20687
21189
  }
20688
21190
  return warnAndSkipMcpServer({
@@ -20692,7 +21194,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20692
21194
  logger
20693
21195
  });
20694
21196
  }
20695
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21197
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20696
21198
  return { enabled: !serverConfig.disabled };
20697
21199
  }
20698
21200
  if (isRemoteMcpServer(serverConfig)) {
@@ -21318,13 +21820,49 @@ const OpencodeMcpRemoteServerSchema = zod_mini.z.looseObject({
21318
21820
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
21319
21821
  enabled: zod_mini.z._default(zod_mini.z.boolean(), true)
21320
21822
  });
21321
- const OpencodeMcpServerSchema = zod_mini.z.union([OpencodeMcpLocalServerSchema, OpencodeMcpRemoteServerSchema]);
21823
+ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21824
+ const OPENCODE_MCP_TRANSPORT_KEYS = [
21825
+ ...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
21826
+ ...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
21827
+ ...OPENCODE_PASSTHROUGH_SERVER_FIELDS
21828
+ ].filter((key) => key !== "enabled");
21829
+ /**
21830
+ * A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
21831
+ * disabling a server another config layer defines. It is the third member of
21832
+ * OpenCode's own `mcp` union in the published schema, described in-source as
21833
+ * "the legacy `{ enabled: false }` form used to disable a server". Without this
21834
+ * arm the union rejects the entry and the whole MCP import aborts — taking
21835
+ * every valid server in the same file down with it.
21836
+ *
21837
+ * Loose, unlike the two transport arms, so a key OpenCode adds to a toggle
21838
+ * later does not bring that abort back — but refined to reject anything
21839
+ * carrying a key only a transport entry has. A plain loose arm would sit under
21840
+ * a malformed `local` or `remote` entry that happens to carry `enabled` and
21841
+ * swallow it, dropping its command, URL, or headers without a word instead of
21842
+ * failing the way it does today. Mirrors {@link KiloMcpToggleSchema}.
21843
+ *
21844
+ * @see https://opencode.ai/config.json
21845
+ */
21846
+ const OpencodeMcpToggleSchema = zod_mini.z.looseObject({ enabled: zod_mini.z.boolean() }).check((0, zod_mini.refine)((entry) => OPENCODE_MCP_TRANSPORT_KEYS.every((key) => !(key in entry)), "not a valid OpenCode MCP server: expected a local server ({type: \"local\", command: [...]}), a remote server ({type: \"remote\", url: \"...\"}), or a bare toggle ({enabled: <bool>}) carrying no field of either"));
21847
+ const OpencodeMcpServerSchema = zod_mini.z.union([
21848
+ OpencodeMcpLocalServerSchema,
21849
+ OpencodeMcpRemoteServerSchema,
21850
+ OpencodeMcpToggleSchema
21851
+ ]);
21322
21852
  const OpencodeConfigSchema = zod_mini.z.looseObject({
21323
21853
  $schema: zod_mini.z.optional(zod_mini.z.string()),
21324
21854
  mcp: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeMcpServerSchema)),
21325
21855
  tools: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.boolean()))
21326
21856
  });
21327
21857
  /**
21858
+ * Tell the two transport arms from a toggle entry. Both carry a `type` literal
21859
+ * and a toggle never does — the schema refuses one that tries — but the toggle
21860
+ * arm is loose, so its index signature hides that from `in` narrowing.
21861
+ */
21862
+ function isOpencodeTransportServer(server) {
21863
+ return server.type === "local" || server.type === "remote";
21864
+ }
21865
+ /**
21328
21866
  * Convert OpenCode native format back to standard MCP format
21329
21867
  * - type: "local" -> "stdio", "remote" -> "sse"
21330
21868
  * - command (array) -> command (first element) + args (rest)
@@ -21349,19 +21887,31 @@ function convertFromOpencodeFormat(opencodeMcp, tools) {
21349
21887
  ...convertOpencodeServers(opencodeMcp, tools)
21350
21888
  };
21351
21889
  }
21890
+ /** Split the shared top-level `tools` map into this server's own two lists. */
21891
+ function splitOpencodeServerTools(serverName, tools) {
21892
+ const enabledTools = [];
21893
+ const disabledTools = [];
21894
+ const prefix = `${serverName}_`;
21895
+ for (const [toolName, enabled] of Object.entries(tools ?? {})) {
21896
+ if (!toolName.startsWith(prefix)) continue;
21897
+ const toolSuffix = toolName.slice(prefix.length);
21898
+ (enabled ? enabledTools : disabledTools).push(toolSuffix);
21899
+ }
21900
+ return {
21901
+ enabledTools,
21902
+ disabledTools
21903
+ };
21904
+ }
21352
21905
  function convertOpencodeServers(opencodeMcp, tools) {
21353
21906
  return Object.fromEntries(Object.entries(opencodeMcp).map(([serverName, serverConfig]) => {
21354
21907
  const extraFields = Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !OPENCODE_KNOWN_SERVER_KEYS.has(key)));
21355
- const enabledTools = [];
21356
- const disabledTools = [];
21357
- const prefix = `${serverName}_`;
21358
- if (tools) {
21359
- for (const [toolName, enabled] of Object.entries(tools)) if (toolName.startsWith(prefix)) {
21360
- const toolSuffix = toolName.slice(prefix.length);
21361
- if (enabled) enabledTools.push(toolSuffix);
21362
- else disabledTools.push(toolSuffix);
21363
- }
21364
- }
21908
+ const { enabledTools, disabledTools } = splitOpencodeServerTools(serverName, tools);
21909
+ if (!isOpencodeTransportServer(serverConfig)) return [serverName, {
21910
+ ...extraFields,
21911
+ disabled: serverConfig.enabled === false,
21912
+ ...enabledTools.length > 0 && { enabledTools },
21913
+ ...disabledTools.length > 0 && { disabledTools }
21914
+ }];
21365
21915
  if (serverConfig.type === "remote") return [serverName, {
21366
21916
  ...extraFields,
21367
21917
  type: "sse",
@@ -21391,7 +21941,20 @@ function convertOpencodeServers(opencodeMcp, tools) {
21391
21941
  }];
21392
21942
  }));
21393
21943
  }
21394
- const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21944
+ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
21945
+ "disabled",
21946
+ "enabledTools",
21947
+ "disabledTools"
21948
+ ]);
21949
+ /**
21950
+ * Warn about the fields a transport-less server loses by being written as a
21951
+ * bare toggle, so the drop is never silent.
21952
+ */
21953
+ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
21954
+ const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
21955
+ if (dropped.length === 0) return;
21956
+ logger?.warn(`OpenCode MCP: "${serverName}" declares no transport, so it is written as a toggle entry and ${dropped.toSorted().join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
21957
+ }
21395
21958
  /**
21396
21959
  * Convert standard MCP format to OpenCode native format
21397
21960
  * - type: "stdio" -> "local", "sse"/"http" -> "remote"
@@ -21401,11 +21964,27 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21401
21964
  * - enabledTools/disabledTools -> top-level tools map (with server name prefix)
21402
21965
  * - OpenCode-supported extras (timeout, oauth) -> passed through verbatim
21403
21966
  */
21404
- function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21967
+ function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, logger) {
21405
21968
  const serverRecord = serverConfig;
21406
21969
  const passthrough = {};
21407
21970
  for (const key of OPENCODE_PASSTHROUGH_SERVER_FIELDS) if (serverRecord[key] !== void 0) passthrough[key] = serverRecord[key];
21408
21971
  const enabled = serverConfig.disabled !== void 0 ? !serverConfig.disabled : true;
21972
+ if (declaresNoTransport(serverConfig)) {
21973
+ if (serverConfig.disabled === void 0) {
21974
+ if (existingEntry !== void 0 && !isOpencodeTransportServer(existingEntry)) {
21975
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21976
+ return existingEntry;
21977
+ }
21978
+ return warnAndSkipMcpServer({
21979
+ toolName: "OpenCode",
21980
+ serverName,
21981
+ reason: "no transport and no enabled state, so there is nothing to toggle",
21982
+ logger
21983
+ });
21984
+ }
21985
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21986
+ return { enabled };
21987
+ }
21409
21988
  if (isRemoteMcpServer(serverConfig)) {
21410
21989
  const url = resolveRemoteMcpUrl(serverConfig);
21411
21990
  if (url === void 0) return warnAndSkipMcpServer({
@@ -21426,7 +22005,7 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21426
22005
  if (commandArray.length === 0) return warnAndSkipMcpServer({
21427
22006
  toolName: "OpenCode",
21428
22007
  serverName,
21429
- reason: declaresNoTransport(serverConfig) ? "no transport at all" : "a local transport but no command",
22008
+ reason: "a local transport but no command",
21430
22009
  logger
21431
22010
  });
21432
22011
  return {
@@ -21438,11 +22017,11 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21438
22017
  ...serverConfig.cwd && { cwd: serverConfig.cwd }
21439
22018
  };
21440
22019
  }
21441
- function convertToOpencodeFormat(mcpServers, logger) {
22020
+ function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
21442
22021
  const tools = {};
21443
22022
  return {
21444
22023
  mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
21445
- const converted = convertServerToOpencodeFormat(serverName, serverConfig, logger);
22024
+ const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], logger);
21446
22025
  if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[`${serverName}_${tool}`] = true;
21447
22026
  if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[`${serverName}_${tool}`] = false;
21448
22027
  return converted === null ? null : [serverName, converted];
@@ -21512,10 +22091,12 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
21512
22091
  fileContent = await readFileContentOrNull(jsonPath);
21513
22092
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
21514
22093
  }
21515
- const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(convertEnvVarRefsToToolFormat({
22094
+ const transformedServers = convertEnvVarRefsToToolFormat({
21516
22095
  mcpServers: rulesyncMcp.getMcpServers(),
21517
22096
  replacement: "{env:$1}"
21518
- }), logger);
22097
+ });
22098
+ const existingMcp = OpencodeConfigSchema.safeParse((0, jsonc_parser.parse)(fileContent || "{}"));
22099
+ const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
21519
22100
  return new OpencodeMcp({
21520
22101
  outputRoot,
21521
22102
  relativeDirPath: basePaths.relativeDirPath,
@@ -23825,10 +24406,11 @@ function buildPermissionEntry$1(toolName, pattern) {
23825
24406
  * file (global scope only). The file holds other CLI settings besides
23826
24407
  * permissions, so it is never deleted.
23827
24408
  *
23828
- * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
23829
- * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
23830
- * authored and round-tripped through the `antigravity-cli` permissions override
23831
- * (see `AntigravityCliPermissionsOverrideSchema`).
24409
+ * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
24410
+ * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
24411
+ * `artifactReviewPolicy` and `allowNonWorkspaceAccess` are authored and
24412
+ * round-tripped through the `antigravity-cli` permissions override (see
24413
+ * `AntigravityCliPermissionsOverrideSchema`).
23832
24414
  */
23833
24415
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
23834
24416
  constructor(params) {
@@ -23892,6 +24474,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23892
24474
  const override = config["antigravity-cli"];
23893
24475
  if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
23894
24476
  if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
24477
+ if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
24478
+ if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
23895
24479
  const fileContent = JSON.stringify(merged, null, 2);
23896
24480
  return new AntigravityCliPermissions({
23897
24481
  outputRoot,
@@ -23918,6 +24502,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23918
24502
  const override = {};
23919
24503
  if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
23920
24504
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
24505
+ if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
24506
+ if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
23921
24507
  const result = { ...config };
23922
24508
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
23923
24509
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -24721,12 +25307,57 @@ function parseClaudePermissionEntry(entry) {
24721
25307
  };
24722
25308
  }
24723
25309
  /**
25310
+ * Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
25311
+ * rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
25312
+ * but never matched by those checks, so Claude Code warns at startup for each
25313
+ * allow, deny, or ask rule in one of these unmatched forms" — so a canonical
25314
+ * `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
25315
+ * docs prescribe instead. A tool-name rule with no path is unaffected: it
25316
+ * matches the tool everywhere and produces no warning.
25317
+ * @see https://code.claude.com/docs/en/permissions
25318
+ */
25319
+ function isPlainRecord(value) {
25320
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25321
+ }
25322
+ /**
25323
+ * Merge `patch` into `base`, recursing into plain objects so a sibling key at
25324
+ * any depth survives. Arrays and scalars are replaced, since a list the author
25325
+ * states is the list they mean.
25326
+ */
25327
+ function deepMergeRecords(base, patch) {
25328
+ const merged = { ...base };
25329
+ for (const [key, value] of Object.entries(patch)) {
25330
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
25331
+ const existing = merged[key];
25332
+ merged[key] = isPlainRecord(existing) && isPlainRecord(value) ? deepMergeRecords(existing, value) : value;
25333
+ }
25334
+ return merged;
25335
+ }
25336
+ const CLAUDE_PATH_RULE_ALIASES = {
25337
+ Write: "Edit",
25338
+ NotebookEdit: "Edit",
25339
+ Glob: "Read"
25340
+ };
25341
+ /**
24724
25342
  * Build a Claude Code permission entry like "Bash(npm run *)".
24725
25343
  * If the pattern is "*", returns just the tool name.
24726
25344
  */
24727
25345
  function buildClaudePermissionEntry(toolName, pattern) {
24728
25346
  if (pattern === "*") return toolName;
24729
- return `${toolName}(${pattern})`;
25347
+ return `${CLAUDE_PATH_RULE_ALIASES[toolName] ?? toolName}(${pattern})`;
25348
+ }
25349
+ /**
25350
+ * The Claude tool names the canonical config manages. Deliberately the tool
25351
+ * names the categories map to and *not* the aliases a path rule is rewritten
25352
+ * to: claiming `Edit` because a `write` rule exists would sweep away the
25353
+ * `Read`/`Edit` entries the ignore feature and the user wrote in the same file.
25354
+ * The rewritten entries are still rulesync's to place — `applyPermissions`
25355
+ * replaces an entry this run emits wherever it currently sits — and the
25356
+ * original name stays claimed so an entry an older rulesync wrote in the warned
25357
+ * form is cleaned up on the next generate.
25358
+ */
25359
+ function managedClaudeToolNames(config) {
25360
+ return new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
24730
25361
  }
24731
25362
  var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions {
24732
25363
  constructor(params) {
@@ -24766,7 +25397,10 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24766
25397
  throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
24767
25398
  }
24768
25399
  const config = rulesyncPermissions.getJson();
24769
- const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
25400
+ const { allow, ask, deny } = convertRulesyncToClaudePermissions({
25401
+ config,
25402
+ logger
25403
+ });
24770
25404
  const overridePermissions = config.claudecode?.permissions;
24771
25405
  if (overridePermissions && typeof overridePermissions === "object") {
24772
25406
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
@@ -24775,7 +25409,9 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24775
25409
  ...nonListFields
24776
25410
  };
24777
25411
  }
24778
- const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
25412
+ const overrideSandbox = config.claudecode?.sandbox;
25413
+ if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
25414
+ const managedToolNames = managedClaudeToolNames(config);
24779
25415
  const merged = applyPermissions({
24780
25416
  settings,
24781
25417
  managedToolNames,
@@ -24809,6 +25445,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24809
25445
  });
24810
25446
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
24811
25447
  if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
25448
+ const { sandbox } = settings;
25449
+ if (isPlainRecord(sandbox) && Object.keys(sandbox).length > 0) config.claudecode = {
25450
+ ...config.claudecode,
25451
+ sandbox
25452
+ };
24812
25453
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
24813
25454
  }
24814
25455
  validate() {
@@ -24830,14 +25471,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24830
25471
  /**
24831
25472
  * Convert rulesync permissions config to Claude Code allow/ask/deny arrays.
24832
25473
  */
24833
- function convertRulesyncToClaudePermissions(config) {
25474
+ function convertRulesyncToClaudePermissions({ config, logger }) {
24834
25475
  const allow = [];
24835
25476
  const ask = [];
24836
25477
  const deny = [];
25478
+ const actionByEntry = /* @__PURE__ */ new Map();
24837
25479
  for (const [category, rules] of Object.entries(config.permission)) {
24838
25480
  const claudeToolName = toClaudeToolName(category);
24839
25481
  for (const [pattern, action] of Object.entries(rules)) {
24840
25482
  const entry = buildClaudePermissionEntry(claudeToolName, pattern);
25483
+ const previous = actionByEntry.get(entry);
25484
+ if (previous !== void 0 && previous !== action) logger?.warn(`Claude Code permissions: rules from different categories both resolve to "${entry}" with conflicting actions (${previous} and ${action}). Both are written; Claude Code applies deny first, then ask, then allow.`);
25485
+ actionByEntry.set(entry, action);
24841
25486
  switch (action) {
24842
25487
  case "allow":
24843
25488
  allow.push(entry);
@@ -25650,19 +26295,24 @@ function mapBashActionToDecision(action) {
25650
26295
  //#endregion
25651
26296
  //#region src/features/permissions/copilot-permissions.ts
25652
26297
  /**
25653
- * The flat, dotted VS Code setting key this adapter manages. VS Code stores
25654
- * settings with dotted keys flat at the document top level, so this is a single
25655
- * literal key — not a nested `chat.tools.terminal` path.
26298
+ * The flat, dotted VS Code setting keys this adapter manages, one per canonical
26299
+ * permission category. VS Code stores settings with dotted keys flat at the
26300
+ * document top level, so each is a single literal key — not a nested
26301
+ * `chat.tools.terminal` path. All three share the same
26302
+ * pattern-to-boolean shape, so one conversion covers them.
26303
+ *
26304
+ * The canonical `read` and `write` categories stay unmapped: VS Code has no
26305
+ * read-approval surface, and folding `write` into the edits map alongside
26306
+ * `edit` would make the two indistinguishable on import.
26307
+ *
25656
26308
  * @see https://code.visualstudio.com/docs/agents/approvals
26309
+ * @see https://code.visualstudio.com/docs/copilot/chat/review-code-edits
25657
26310
  */
25658
- const AUTO_APPROVE_KEY = "chat.tools.terminal.autoApprove";
25659
- /**
25660
- * The canonical permission category this adapter maps. Only shell/terminal
25661
- * commands (`bash`) have a clean, high-fidelity representation in VS Code's
25662
- * `chat.tools.terminal.autoApprove` map; other categories (read/edit/webfetch/
25663
- * …) have no terminal-command equivalent and are intentionally not mapped.
25664
- */
25665
- const TERMINAL_CATEGORY = "bash";
26311
+ const AUTO_APPROVE_KEYS = {
26312
+ bash: "chat.tools.terminal.autoApprove",
26313
+ edit: "chat.tools.edits.autoApprove",
26314
+ webfetch: "chat.tools.urls.autoApprove"
26315
+ };
25666
26316
  function asAutoApproveMap(value) {
25667
26317
  if (!isPlainObject$1(value)) return {};
25668
26318
  const result = {};
@@ -25670,6 +26320,21 @@ function asAutoApproveMap(value) {
25670
26320
  return result;
25671
26321
  }
25672
26322
  /**
26323
+ * Render one canonical category's rules as a VS Code auto-approve map. Returns
26324
+ * `undefined` when the category contributes nothing, so the key is retracted
26325
+ * rather than written as an empty object.
26326
+ *
26327
+ * The resulting map replaces the file's existing value wholesale — rulesync
26328
+ * owns these keys, so a rule dropped from the canonical config disappears from
26329
+ * the settings file too.
26330
+ */
26331
+ function buildAutoApproveValue(rules) {
26332
+ const autoApprove = {};
26333
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
26334
+ else if (action === "deny") autoApprove[pattern] = false;
26335
+ return Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
26336
+ }
26337
+ /**
25673
26338
  * Permissions generator for GitHub Copilot Chat in VS Code.
25674
26339
  *
25675
26340
  * VS Code has no standalone, environment-agnostic Copilot policy file (like
@@ -25679,13 +26344,16 @@ function asAutoApproveMap(value) {
25679
26344
  * many unrelated keys, so reads and writes merge into the existing JSON
25680
26345
  * (touching only the one managed key) and the file is never deleted.
25681
26346
  *
25682
- * Scope is deliberately limited to `chat.tools.terminal.autoApprove` the one
25683
- * clean, non-lossy mapping. The canonical `bash` category's per-pattern rules
25684
- * map as: `allow` → `true` (auto-approve), `deny` → `false` (never approve),
25685
- * and `ask` → the entry is OMITTED (VS Code then falls through to its default
25686
- * in-chat approval prompt, i.e. "ask"). Only project scope is modeled: VS Code's
25687
- * user-scope settings.json lives at a platform-dependent path outside rulesync's
25688
- * home-relative global model.
26347
+ * Three canonical categories have a clean, non-lossy representation and are
26348
+ * mapped (see {@link AUTO_APPROVE_KEYS}): `bash`, `edit` and `webfetch`. In
26349
+ * every one, per-pattern rules map as: `allow` → `true` (auto-approve), `deny`
26350
+ * `false` (VS Code then always prompts note this is "never auto-approve",
26351
+ * not a hard block), and `ask` the entry is OMITTED (VS Code falls through to
26352
+ * the same default prompt). A key whose canonical category is absent entirely
26353
+ * is left untouched, so authoring only `bash` rules never disturbs a
26354
+ * hand-written edits or urls map.
26355
+ * Only project scope is modeled: VS Code's user-scope settings.json lives at a
26356
+ * platform-dependent path outside rulesync's home-relative global model.
25689
26357
  */
25690
26358
  var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25691
26359
  constructor(params) {
@@ -25722,11 +26390,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25722
26390
  const paths = CopilotPermissions.getSettablePaths();
25723
26391
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25724
26392
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
25725
- const rules = rulesyncPermissions.getJson().permission[TERMINAL_CATEGORY] ?? {};
25726
- const autoApprove = {};
25727
- for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
25728
- else if (action === "deny") autoApprove[pattern] = false;
25729
- const patchValue = Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
26393
+ const config = rulesyncPermissions.getJson();
26394
+ const patch = {};
26395
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
26396
+ const rules = config.permission[category];
26397
+ if (rules === void 0) continue;
26398
+ patch[settingKey] = buildAutoApproveValue(rules);
26399
+ }
25730
26400
  return new CopilotPermissions({
25731
26401
  outputRoot,
25732
26402
  relativeDirPath: paths.relativeDirPath,
@@ -25735,7 +26405,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25735
26405
  fileKey: sharedConfigFileKey(paths),
25736
26406
  feature: "permissions",
25737
26407
  existingContent,
25738
- patch: { [AUTO_APPROVE_KEY]: patchValue },
26408
+ patch,
25739
26409
  filePath
25740
26410
  }),
25741
26411
  validate: true
@@ -25754,10 +26424,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25754
26424
  } catch (error) {
25755
26425
  throw new Error(`Failed to parse Copilot VS Code settings in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
25756
26426
  }
25757
- const autoApprove = asAutoApproveMap(settings[AUTO_APPROVE_KEY]);
25758
- const rules = {};
25759
- for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
25760
- const permission = Object.keys(rules).length > 0 ? { [TERMINAL_CATEGORY]: rules } : {};
26427
+ const permission = {};
26428
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
26429
+ const autoApprove = asAutoApproveMap(settings[settingKey]);
26430
+ const rules = {};
26431
+ for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
26432
+ if (Object.keys(rules).length > 0) permission[category] = rules;
26433
+ }
25761
26434
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
25762
26435
  }
25763
26436
  validate() {
@@ -31777,6 +32450,7 @@ const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
31777
32450
  arguments: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
31778
32451
  context: zod_mini.z.optional(zod_mini.z.string()),
31779
32452
  agent: zod_mini.z.optional(zod_mini.z.string()),
32453
+ background: zod_mini.z.optional(zod_mini.z.boolean()),
31780
32454
  hooks: zod_mini.z.optional(zod_mini.z.looseObject({})),
31781
32455
  shell: zod_mini.z.optional(zod_mini.z.string()),
31782
32456
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
@@ -31803,6 +32477,7 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
31803
32477
  shell: section.shell
31804
32478
  };
31805
32479
  const definedFields = {
32480
+ background: section.background,
31806
32481
  arguments: section.arguments,
31807
32482
  hooks: section.hooks,
31808
32483
  "disable-model-invocation": resolvedDisableModelInvocation,
@@ -31880,6 +32555,7 @@ var ClaudecodeSkill = class extends ToolSkill {
31880
32555
  ...frontmatter.arguments !== void 0 && { arguments: frontmatter.arguments },
31881
32556
  ...frontmatter.context && { context: frontmatter.context },
31882
32557
  ...frontmatter.agent && { agent: frontmatter.agent },
32558
+ ...frontmatter.background !== void 0 && { background: frontmatter.background },
31883
32559
  ...frontmatter.hooks !== void 0 && { hooks: frontmatter.hooks },
31884
32560
  ...frontmatter.shell && { shell: frontmatter.shell },
31885
32561
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
@@ -32489,7 +33165,9 @@ const CopilotcliSkillFrontmatterSchema = zod_mini.z.looseObject({
32489
33165
  description: zod_mini.z.string(),
32490
33166
  license: zod_mini.z.optional(zod_mini.z.string()),
32491
33167
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
32492
- "argument-hint": zod_mini.z.optional(zod_mini.z.string())
33168
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
33169
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
33170
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
32493
33171
  });
32494
33172
  /**
32495
33173
  * Represents a GitHub Copilot CLI skill directory.
@@ -32548,7 +33226,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32548
33226
  const copilotcliSection = {
32549
33227
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
32550
33228
  ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
32551
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
33229
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
33230
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
33231
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
32552
33232
  };
32553
33233
  const rulesyncFrontmatter = {
32554
33234
  name: frontmatter.name,
@@ -32575,7 +33255,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32575
33255
  description: rulesyncFrontmatter.description,
32576
33256
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
32577
33257
  ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
32578
- ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
33258
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] },
33259
+ ...rulesyncFrontmatter.copilotcli?.["user-invocable"] !== void 0 && { "user-invocable": rulesyncFrontmatter.copilotcli["user-invocable"] },
33260
+ ...rulesyncFrontmatter.copilotcli?.["disable-model-invocation"] !== void 0 && { "disable-model-invocation": rulesyncFrontmatter.copilotcli["disable-model-invocation"] }
32579
33261
  };
32580
33262
  return new CopilotcliSkill({
32581
33263
  outputRoot,
@@ -34245,6 +34927,37 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
34245
34927
  alternativeSkillRoots: [global ? OPENCODE_GLOBAL_SKILL_DIR_PATH : OPENCODE_SKILL_DIR_PATH]
34246
34928
  };
34247
34929
  }
34930
+ /**
34931
+ * Extra skill roots the project configured in `opencode.json` /
34932
+ * `opencode.jsonc` via `skills.paths` ("Additional paths to skill folders").
34933
+ * Without these, skills a project keeps outside `.opencode/skills/` are
34934
+ * invisible to `rulesync import` even though OpenCode loads them.
34935
+ *
34936
+ * Import-only: rulesync keeps writing to its own managed root, so a
34937
+ * configured path is read but never generated into. `skills.urls` is a
34938
+ * remote-fetch surface and is out of scope for a file-based generator.
34939
+ *
34940
+ * Absolute paths and paths escaping the output root are dropped — an import
34941
+ * root is joined onto `outputRoot`, and reaching outside it is not something
34942
+ * a project config should be able to ask for.
34943
+ *
34944
+ * @see https://opencode.ai/config.json
34945
+ */
34946
+ static async getConfiguredImportRoots({ outputRoot, global = false }) {
34947
+ const skills = asOpencodeEntries((await readOpencodeConfig({
34948
+ outputRoot,
34949
+ global
34950
+ })).skills);
34951
+ if (skills === null || !Array.isArray(skills.paths)) return [];
34952
+ const configDir = getOpencodeConfigDir({
34953
+ outputRoot,
34954
+ global
34955
+ });
34956
+ return skills.paths.filter((candidate) => typeof candidate === "string" && candidate !== "" && !(0, node_path.isAbsolute)(candidate) && !(0, node_path.normalize)(candidate).startsWith("..")).map((relativeDirPath) => ({
34957
+ outputRoot: configDir,
34958
+ relativeDirPath
34959
+ }));
34960
+ }
34248
34961
  getFrontmatter() {
34249
34962
  return OpenCodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
34250
34963
  }
@@ -36178,12 +36891,19 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36178
36891
  */
36179
36892
  async loadToolDirs() {
36180
36893
  const factory = this.getFactory(this.toolTarget);
36181
- const roots = toolSkillImportRoots(factory.class.getSettablePaths({ global: this.global }));
36894
+ const paths = factory.class.getSettablePaths({ global: this.global });
36895
+ const configuredRoots = factory.class.getConfiguredImportRoots ? await factory.class.getConfiguredImportRoots({
36896
+ outputRoot: this.outputRoot,
36897
+ global: this.global
36898
+ }) : [];
36899
+ const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
36900
+ const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
36182
36901
  const seenSkillNames = /* @__PURE__ */ new Set();
36183
36902
  const toolSkills = [];
36184
36903
  for (const root of roots) {
36185
36904
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
36186
36905
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
36906
+ const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
36187
36907
  const skillsDirPath = (0, node_path.join)(rootOutputRoot, relativeDirPath);
36188
36908
  if (!await directoryExists(skillsDirPath)) continue;
36189
36909
  const dirPaths = await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*"), { type: "dir" });
@@ -36198,12 +36918,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36198
36918
  })) continue;
36199
36919
  ownedDirNames.push(dirName);
36200
36920
  }
36201
- const directorySkills = await Promise.all(ownedDirNames.map((dirName) => factory.class.fromDir({
36202
- outputRoot: rootOutputRoot,
36203
- relativeDirPath,
36204
- dirName,
36205
- global: this.global
36206
- })));
36921
+ const directorySkills = (await Promise.all(ownedDirNames.map(async (dirName) => {
36922
+ try {
36923
+ return await factory.class.fromDir({
36924
+ outputRoot: rootOutputRoot,
36925
+ relativeDirPath,
36926
+ dirName,
36927
+ global: this.global
36928
+ });
36929
+ } catch (error) {
36930
+ if (!isConfiguredRoot) throw error;
36931
+ this.logger.warn(`Skipping ${(0, node_path.join)(relativeDirPath, dirName)}: ${formatError(error)}`);
36932
+ return null;
36933
+ }
36934
+ }))).filter((skill) => skill !== null);
36207
36935
  for (const skill of directorySkills) {
36208
36936
  const skillName = skill.getImportIdentity();
36209
36937
  if (seenSkillNames.has(skillName)) continue;
@@ -36728,6 +37456,242 @@ var RovodevSubagent = class RovodevSubagent extends ToolSubagent {
36728
37456
  }
36729
37457
  };
36730
37458
  //#endregion
37459
+ //#region src/features/subagents/antigravity-shared-subagent.ts
37460
+ /**
37461
+ * Frontmatter of an Antigravity custom agent (Markdown format, CLI v1.1.6+).
37462
+ *
37463
+ * `name` and `description` are required upstream; the rest are optional and
37464
+ * documented with defaults (`tools: []`, `mainAgent: true`, `subagent: true`,
37465
+ * `model: inherit`, `commandExecutionPolicy: sandbox`, `mcpServers: []`,
37466
+ * `skills`/`plugins`: `[]`). `hidden` and `inheritMcp` appear in the v1.1.6
37467
+ * release notes but not in the documented frontmatter table, so they are
37468
+ * accepted as verbatim passthrough without any behavior modeled around them.
37469
+ * `looseObject` keeps unknown future fields round-tripping.
37470
+ *
37471
+ * @see https://antigravity.google/docs/subagents
37472
+ */
37473
+ const AntigravitySubagentFrontmatterSchema = zod_mini.z.looseObject({
37474
+ name: zod_mini.z.string(),
37475
+ description: zod_mini.z.string().check(zod_mini.z.minLength(1)),
37476
+ tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
37477
+ mainAgent: zod_mini.z.optional(zod_mini.z.boolean()),
37478
+ subagent: zod_mini.z.optional(zod_mini.z.boolean()),
37479
+ model: zod_mini.z.optional(zod_mini.z.string()),
37480
+ commandExecutionPolicy: zod_mini.z.optional(zod_mini.z.string()),
37481
+ mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.unknown())),
37482
+ skills: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
37483
+ plugins: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
37484
+ hidden: zod_mini.z.optional(zod_mini.z.boolean()),
37485
+ inheritMcp: zod_mini.z.optional(zod_mini.z.boolean())
37486
+ });
37487
+ /**
37488
+ * Shared custom-agent (subagent) implementation for Google Antigravity 2.0,
37489
+ * used by the IDE, the CLI and plugin bundles.
37490
+ *
37491
+ * Antigravity discovers agents at `.agents/agents/<name>.md` (project) and
37492
+ * `~/.gemini/config/agents/<name>.md` (global, shared by the IDE and the CLI
37493
+ * exactly like `~/.gemini/config/hooks.json`). The directory form
37494
+ * (`<name>/agent.md`) is an equivalent alternative upstream; rulesync emits and
37495
+ * imports the flat file form. The body after the frontmatter is the agent's
37496
+ * system prompt.
37497
+ *
37498
+ * Concrete subclasses only supply the rulesync target name they answer to via
37499
+ * {@link AntigravitySharedSubagent.getToolTarget} and, where the shared file is
37500
+ * not involved, the sections they read via
37501
+ * {@link AntigravitySharedSubagent.getReadSectionKeys}.
37502
+ *
37503
+ * @see https://antigravity.google/docs/subagents
37504
+ */
37505
+ var AntigravitySharedSubagent = class extends ToolSubagent {
37506
+ frontmatter;
37507
+ body;
37508
+ constructor({ frontmatter, body, fileContent, ...rest }) {
37509
+ if (rest.validate !== false) {
37510
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
37511
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
37512
+ }
37513
+ super({
37514
+ ...rest,
37515
+ fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
37516
+ });
37517
+ this.frontmatter = frontmatter;
37518
+ this.body = body;
37519
+ }
37520
+ /** The rulesync target name this subagent answers to. */
37521
+ static getToolTarget() {
37522
+ throw new Error("Please implement this method in the subclass.");
37523
+ }
37524
+ /**
37525
+ * Tool-specific sections this target reads, in increasing precedence order.
37526
+ *
37527
+ * `antigravity-ide` and `antigravity-cli` write the very same file, so a
37528
+ * target that read only its own section would silently drop the other's keys
37529
+ * — and which one survived would depend on `--targets` order. Every target
37530
+ * therefore merges the shared `antigravity-ide` → `antigravity-cli` sections
37531
+ * (the CLI block wins, matching the fixed order the MCP feature already uses
37532
+ * for the same shared-output reason), and the plugin target layers its own
37533
+ * section on top of that. Only `getToolTarget()` decides which section an
37534
+ * import writes back into.
37535
+ */
37536
+ static getReadSectionKeys() {
37537
+ return ["antigravity-ide", "antigravity-cli"];
37538
+ }
37539
+ static getSettablePaths({ global = false } = {}) {
37540
+ return { relativeDirPath: global ? ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH : ANTIGRAVITY_AGENTS_DIR_PATH };
37541
+ }
37542
+ getFrontmatter() {
37543
+ return this.frontmatter;
37544
+ }
37545
+ getBody() {
37546
+ return this.body;
37547
+ }
37548
+ toRulesyncSubagent() {
37549
+ const { name, description, ...restFields } = this.frontmatter;
37550
+ return new RulesyncSubagent({
37551
+ outputRoot: ".",
37552
+ frontmatter: {
37553
+ targets: ["*"],
37554
+ name,
37555
+ description,
37556
+ ...Object.keys(restFields).length > 0 && { [this.constructor.getToolTarget()]: restFields }
37557
+ },
37558
+ body: this.body,
37559
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
37560
+ relativeFilePath: this.getRelativeFilePath(),
37561
+ validate: true
37562
+ });
37563
+ }
37564
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
37565
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
37566
+ const mergedSection = Object.assign({}, ...this.getReadSectionKeys().map((key) => rulesyncFrontmatter[key] ?? {}));
37567
+ const toolSection = this.filterToolSpecificSection(mergedSection, ["name", "description"]);
37568
+ const rawFrontmatter = {
37569
+ name: rulesyncFrontmatter.name,
37570
+ description: rulesyncFrontmatter.description || `${rulesyncFrontmatter.name} subagent`,
37571
+ ...toolSection
37572
+ };
37573
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(rawFrontmatter);
37574
+ if (!result.success) throw new Error(`Invalid ${this.getToolTarget()} subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
37575
+ const frontmatter = result.data;
37576
+ const body = rulesyncSubagent.getBody();
37577
+ const paths = this.getSettablePaths({ global });
37578
+ return new this({
37579
+ outputRoot,
37580
+ frontmatter,
37581
+ body,
37582
+ relativeDirPath: paths.relativeDirPath,
37583
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
37584
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
37585
+ validate,
37586
+ global
37587
+ });
37588
+ }
37589
+ validate() {
37590
+ if (!this.frontmatter) return {
37591
+ success: true,
37592
+ error: null
37593
+ };
37594
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(this.frontmatter);
37595
+ if (result.success) return {
37596
+ success: true,
37597
+ error: null
37598
+ };
37599
+ return {
37600
+ success: false,
37601
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
37602
+ };
37603
+ }
37604
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
37605
+ return this.isTargetedByRulesyncSubagentDefault({
37606
+ rulesyncSubagent,
37607
+ toolTarget: this.getToolTarget()
37608
+ });
37609
+ }
37610
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
37611
+ const paths = this.getSettablePaths({ global });
37612
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
37613
+ const fileContent = await readFileContent(filePath);
37614
+ const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
37615
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
37616
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
37617
+ return new this({
37618
+ outputRoot,
37619
+ relativeDirPath: paths.relativeDirPath,
37620
+ relativeFilePath,
37621
+ frontmatter: result.data,
37622
+ body: content.trim(),
37623
+ fileContent,
37624
+ validate,
37625
+ global
37626
+ });
37627
+ }
37628
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
37629
+ return new this({
37630
+ outputRoot,
37631
+ relativeDirPath,
37632
+ relativeFilePath,
37633
+ frontmatter: {
37634
+ name: "",
37635
+ description: ""
37636
+ },
37637
+ body: "",
37638
+ fileContent: "",
37639
+ validate: false,
37640
+ global
37641
+ });
37642
+ }
37643
+ };
37644
+ //#endregion
37645
+ //#region src/features/subagents/antigravity-cli-subagent.ts
37646
+ /**
37647
+ * Google Antigravity CLI custom agent (subagent), shipped in CLI v1.1.6.
37648
+ *
37649
+ * Shares all behavior with {@link AntigravitySharedSubagent}; the CLI reads the
37650
+ * same `.agents/agents/` and `~/.gemini/config/agents/` roots as the IDE and
37651
+ * exposes them through the `agy agents` subcommand and the `--agent` flag. It
37652
+ * answers to the `antigravity-cli` target.
37653
+ */
37654
+ var AntigravityCliSubagent = class extends AntigravitySharedSubagent {
37655
+ static getToolTarget() {
37656
+ return "antigravity-cli";
37657
+ }
37658
+ };
37659
+ //#endregion
37660
+ //#region src/features/subagents/antigravity-ide-subagent.ts
37661
+ /**
37662
+ * Google Antigravity IDE custom agent (subagent).
37663
+ *
37664
+ * Shares all behavior with {@link AntigravitySharedSubagent} — the subagents
37665
+ * documentation is product-wide and lists the same `.agents/agents/` and
37666
+ * `~/.gemini/config/agents/` roots for the IDE and the CLI. It answers to the
37667
+ * `antigravity-ide` target.
37668
+ */
37669
+ var AntigravityIdeSubagent = class extends AntigravitySharedSubagent {
37670
+ static getToolTarget() {
37671
+ return "antigravity-ide";
37672
+ }
37673
+ };
37674
+ //#endregion
37675
+ //#region src/features/subagents/antigravity-plugin-subagent.ts
37676
+ /**
37677
+ * Custom agent inside an Antigravity plugin bundle
37678
+ * (`<plugin_name>/agents/<name>.md`). Plugin bundles are project-scope output
37679
+ * only; they are staged into `~/.gemini/antigravity-cli/plugins/` by the user.
37680
+ *
37681
+ * @see https://antigravity.google/docs/cli/plugins
37682
+ */
37683
+ var AntigravityPluginSubagent = class extends AntigravitySharedSubagent {
37684
+ static getToolTarget() {
37685
+ return "antigravity-plugin";
37686
+ }
37687
+ static getReadSectionKeys() {
37688
+ return [...super.getReadSectionKeys(), "antigravity-plugin"];
37689
+ }
37690
+ static getSettablePaths() {
37691
+ return { relativeDirPath: ANTIGRAVITY_PLUGIN_AGENTS_DIR };
37692
+ }
37693
+ };
37694
+ //#endregion
36731
37695
  //#region src/features/subagents/augmentcode-subagent.ts
36732
37696
  const AugmentcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
36733
37697
  name: zod_mini.z.string(),
@@ -40305,6 +41269,30 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
40305
41269
  filePattern: "*.md"
40306
41270
  }
40307
41271
  }],
41272
+ ["antigravity-cli", {
41273
+ class: AntigravityCliSubagent,
41274
+ meta: {
41275
+ supportsSimulated: false,
41276
+ supportsGlobal: true,
41277
+ filePattern: "*.md"
41278
+ }
41279
+ }],
41280
+ ["antigravity-ide", {
41281
+ class: AntigravityIdeSubagent,
41282
+ meta: {
41283
+ supportsSimulated: false,
41284
+ supportsGlobal: true,
41285
+ filePattern: "*.md"
41286
+ }
41287
+ }],
41288
+ ["antigravity-plugin", {
41289
+ class: AntigravityPluginSubagent,
41290
+ meta: {
41291
+ supportsSimulated: false,
41292
+ supportsGlobal: false,
41293
+ filePattern: "*.md"
41294
+ }
41295
+ }],
40308
41296
  ["augmentcode", {
40309
41297
  class: AugmentcodeSubagent,
40310
41298
  meta: {
@@ -42518,6 +43506,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
42518
43506
  const CopilotRuleFrontmatterSchema = zod_mini.z.object({
42519
43507
  description: zod_mini.z.optional(zod_mini.z.string()),
42520
43508
  applyTo: zod_mini.z.optional(zod_mini.z.string()),
43509
+ name: zod_mini.z.optional(zod_mini.z.string()),
42521
43510
  excludeAgent: zod_mini.z.optional(zod_mini.z.union([
42522
43511
  zod_mini.z.literal("code-review"),
42523
43512
  zod_mini.z.literal("cloud-agent"),
@@ -42575,7 +43564,10 @@ var CopilotRule = class CopilotRule extends ToolRule {
42575
43564
  root: this.isRoot(),
42576
43565
  description: this.frontmatter.description,
42577
43566
  globs,
42578
- ...this.frontmatter.excludeAgent && { copilot: { excludeAgent: this.frontmatter.excludeAgent } }
43567
+ ...(this.frontmatter.excludeAgent || this.frontmatter.name) && { copilot: {
43568
+ ...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
43569
+ ...this.frontmatter.name && { name: this.frontmatter.name }
43570
+ } }
42579
43571
  };
42580
43572
  const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
42581
43573
  return new RulesyncRule({
@@ -42594,7 +43586,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
42594
43586
  const copilotFrontmatter = {
42595
43587
  description: rulesyncFrontmatter.description,
42596
43588
  applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0,
42597
- excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent
43589
+ excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
43590
+ name: rulesyncFrontmatter.copilot?.name
42598
43591
  };
42599
43592
  const body = rulesyncRule.getBody();
42600
43593
  if (root) return new CopilotRule({