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.
@@ -1,7 +1,7 @@
1
1
  import { ZodError } from "zod";
2
2
  import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
3
3
  import { cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
4
- import path, { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
4
+ import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
5
5
  import { parse, printParseErrorCode } from "jsonc-parser";
6
6
  import os from "node:os";
7
7
  import { intersection, kebabCase, uniq } from "es-toolkit";
@@ -268,6 +268,9 @@ const commandsProcessorToolTargetTuple = [
268
268
  const subagentsProcessorToolTargetTuple = [
269
269
  "kilo",
270
270
  "agentsmd",
271
+ "antigravity-cli",
272
+ "antigravity-ide",
273
+ "antigravity-plugin",
271
274
  "augmentcode",
272
275
  "claudecode",
273
276
  "claudecode-plugin",
@@ -399,6 +402,7 @@ const permissionsProcessorToolTargetTuple = [
399
402
  ];
400
403
  const checksProcessorToolTargetTuple = [
401
404
  "amp",
405
+ "cursor",
402
406
  "hermesagent",
403
407
  "takt"
404
408
  ];
@@ -2109,7 +2113,10 @@ const HookDefinitionSchema = z.looseObject({
2109
2113
  model: z.optional(safeString),
2110
2114
  args: z.optional(z.array(safeString)),
2111
2115
  metadata: z.optional(z.looseObject({})),
2112
- if: z.optional(safeString)
2116
+ if: z.optional(safeString),
2117
+ commandWindows: z.optional(safeString),
2118
+ asyncRewake: z.optional(z.boolean()),
2119
+ continueOnBlock: z.optional(z.boolean())
2113
2120
  });
2114
2121
  /**
2115
2122
  * All canonical hook event names.
@@ -2163,6 +2170,7 @@ const HOOK_EVENTS = [
2163
2170
  "configChange",
2164
2171
  "cwdChanged",
2165
2172
  "fileChanged",
2173
+ "directoryAdded",
2166
2174
  "elicitation",
2167
2175
  "elicitationResult"
2168
2176
  ];
@@ -2224,6 +2232,7 @@ const CLAUDE_HOOK_EVENTS = [
2224
2232
  "configChange",
2225
2233
  "cwdChanged",
2226
2234
  "fileChanged",
2235
+ "directoryAdded",
2227
2236
  "postCompact",
2228
2237
  "elicitation",
2229
2238
  "elicitationResult"
@@ -2256,7 +2265,15 @@ const DEVIN_HOOK_EVENTS = [
2256
2265
  "permissionRequest",
2257
2266
  "postCompact"
2258
2267
  ];
2259
- /** Hook events supported by OpenCode. */
2268
+ /**
2269
+ * Hook events supported by OpenCode.
2270
+ *
2271
+ * `preCompact` maps to `experimental.session.compacting`, which the plugin docs
2272
+ * document as a named `(input, output)` hook rather than an `event.type`
2273
+ * dispatch; the other entries are all generic events.
2274
+ *
2275
+ * @see https://opencode.ai/docs/plugins/
2276
+ */
2260
2277
  const OPENCODE_HOOK_EVENTS = [
2261
2278
  "sessionStart",
2262
2279
  "preToolUse",
@@ -2264,9 +2281,19 @@ const OPENCODE_HOOK_EVENTS = [
2264
2281
  "stop",
2265
2282
  "afterFileEdit",
2266
2283
  "afterShellExecution",
2267
- "permissionRequest"
2284
+ "permissionRequest",
2285
+ "preCompact",
2286
+ "postCompact",
2287
+ "afterError",
2288
+ "fileChanged"
2268
2289
  ];
2269
- /** Hook events supported by Kilo. (Currently identical to OpenCode) */
2290
+ /**
2291
+ * Hook events supported by Kilo. Identical to OpenCode: Kilo's plugin docs list
2292
+ * the same event surface, including `session.compacted`, `session.error`,
2293
+ * `file.watcher.updated` and the experimental compaction hook.
2294
+ *
2295
+ * @see https://kilo.ai/docs/automate/extending/plugins
2296
+ */
2270
2297
  const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS;
2271
2298
  /**
2272
2299
  * Hook events supported by Pi Coding Agent, bridged through a generated
@@ -2333,7 +2360,8 @@ const COPILOT_HOOK_EVENTS = [
2333
2360
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
2334
2361
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
2335
2362
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
2336
- * `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
2363
+ * `notification`, `userPromptTransformed` ← `userPromptExpansion`,
2364
+ * `preMcpToolCall` ← `beforeMCPExecution`.
2337
2365
  *
2338
2366
  * `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
2339
2367
  * v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
@@ -2355,6 +2383,7 @@ const COPILOTCLI_HOOK_EVENTS = [
2355
2383
  "preCompact",
2356
2384
  "permissionRequest",
2357
2385
  "notification",
2386
+ "userPromptExpansion",
2358
2387
  "beforeMCPExecution"
2359
2388
  ];
2360
2389
  /**
@@ -2399,6 +2428,7 @@ const DEEPAGENTS_HOOK_EVENTS = [
2399
2428
  /** Hook events supported by Codex CLI. */
2400
2429
  const CODEXCLI_HOOK_EVENTS = [
2401
2430
  "sessionStart",
2431
+ "sessionEnd",
2402
2432
  "preToolUse",
2403
2433
  "postToolUse",
2404
2434
  "beforeSubmitPrompt",
@@ -2833,6 +2863,7 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
2833
2863
  configChange: "ConfigChange",
2834
2864
  cwdChanged: "CwdChanged",
2835
2865
  fileChanged: "FileChanged",
2866
+ directoryAdded: "DirectoryAdded",
2836
2867
  postCompact: "PostCompact",
2837
2868
  elicitation: "Elicitation",
2838
2869
  elicitationResult: "ElicitationResult"
@@ -2954,7 +2985,11 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
2954
2985
  stop: "session.idle",
2955
2986
  afterFileEdit: "file.edited",
2956
2987
  afterShellExecution: "command.executed",
2957
- permissionRequest: "permission.asked"
2988
+ permissionRequest: "permission.asked",
2989
+ preCompact: "experimental.session.compacting",
2990
+ postCompact: "session.compacted",
2991
+ afterError: "session.error",
2992
+ fileChanged: "file.watcher.updated"
2958
2993
  };
2959
2994
  /**
2960
2995
  * Map canonical camelCase event names to Kilo dot-notation.
@@ -3030,6 +3065,7 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
3030
3065
  preCompact: "preCompact",
3031
3066
  permissionRequest: "permissionRequest",
3032
3067
  notification: "notification",
3068
+ userPromptExpansion: "userPromptTransformed",
3033
3069
  beforeMCPExecution: "preMcpToolCall"
3034
3070
  };
3035
3071
  /** Map GitHub Copilot CLI event names back to canonical camelCase. */
@@ -3039,6 +3075,7 @@ const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
3039
3075
  */
3040
3076
  const CANONICAL_TO_CODEXCLI_EVENT_NAMES = {
3041
3077
  sessionStart: "SessionStart",
3078
+ sessionEnd: "SessionEnd",
3042
3079
  preToolUse: "PreToolUse",
3043
3080
  postToolUse: "PostToolUse",
3044
3081
  beforeSubmitPrompt: "UserPromptSubmit",
@@ -3461,6 +3498,25 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
3461
3498
  //#endregion
3462
3499
  //#region src/types/mcp.ts
3463
3500
  const EnvVarNameSchema = z.string().check(refine((value) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(value), "envVars entries must be valid environment variable names"));
3501
+ /**
3502
+ * One `envVars` entry. A bare name reads the variable from Codex's own
3503
+ * environment; the object form names the environment to read it from, where
3504
+ * `source = "remote"` reads from the remote executor environment.
3505
+ * @see https://learn.chatgpt.com/docs/extend/mcp
3506
+ */
3507
+ const EnvVarEntrySchema = z.union([EnvVarNameSchema, z.strictObject({
3508
+ name: EnvVarNameSchema,
3509
+ source: z.optional(z.enum(["local", "remote"]))
3510
+ })]);
3511
+ /**
3512
+ * Whether a value is usable as `envVars`. Applied in both directions by the
3513
+ * codex adapter, so an entry read out of somebody's `config.toml` can never be
3514
+ * imported into a `.rulesync/mcp.jsonc` that the next generate would refuse to
3515
+ * parse.
3516
+ */
3517
+ function isEnvVarEntryArray(value) {
3518
+ return Array.isArray(value) && value.every((entry) => EnvVarEntrySchema.safeParse(entry).success);
3519
+ }
3464
3520
  const McpServerSchema = z.looseObject({
3465
3521
  type: z.optional(z.enum([
3466
3522
  "local",
@@ -3475,7 +3531,8 @@ const McpServerSchema = z.looseObject({
3475
3531
  url: z.optional(z.string()),
3476
3532
  httpUrl: z.optional(z.string()),
3477
3533
  env: z.optional(z.record(z.string(), z.string())),
3478
- envVars: z.optional(z.array(EnvVarNameSchema)),
3534
+ envVars: z.optional(z.array(EnvVarEntrySchema)),
3535
+ experimentalEnvironment: z.optional(z.string()),
3479
3536
  disabled: z.optional(z.boolean()),
3480
3537
  networkTimeout: z.optional(z.number()),
3481
3538
  timeout: z.optional(z.number()),
@@ -3641,6 +3698,8 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3641
3698
  "description",
3642
3699
  "exposed",
3643
3700
  "envVars",
3701
+ "experimentalEnvironment",
3702
+ "experimental_environment",
3644
3703
  "enabled"
3645
3704
  ])];
3646
3705
  }));
@@ -3943,12 +4002,23 @@ const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
3943
4002
  * authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
3944
4003
  * ignored here (rulesync owns them).
3945
4004
  *
4005
+ * `sandbox` is the sibling top-level settings subtree that governs the sandbox
4006
+ * Claude Code runs commands in (`sandbox.network.*`, `sandbox.filesystem.*`,
4007
+ * `sandbox.credentials`, `sandbox.allowAppleEvents`, ...). It has no canonical
4008
+ * permission category either — it constrains how a permitted command runs
4009
+ * rather than which commands are permitted — so it is a loose passthrough on
4010
+ * the same terms, merged into the top level of `.claude/settings.json`.
4011
+ *
3946
4012
  * @example
3947
4013
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
4014
+ * @example
4015
+ * { "sandbox": { "network": { "allowedDomains": ["example.com"], "strictAllowlist": true } } }
4016
+ * @see https://code.claude.com/docs/en/sandboxing
3948
4017
  */
3949
4018
  const ClaudecodePermissionsOverrideSchema = z.looseObject({
3950
4019
  permission: z.optional(ToolScopedPermissionSchema),
3951
- permissions: z.optional(z.looseObject({}))
4020
+ permissions: z.optional(z.looseObject({})),
4021
+ sandbox: z.optional(z.looseObject({}))
3952
4022
  });
3953
4023
  /**
3954
4024
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
@@ -4221,19 +4291,24 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4221
4291
  });
4222
4292
  /**
4223
4293
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4224
- * `settings.json` carries two global autonomy/sandbox knobs outside the
4294
+ * `settings.json` carries four global autonomy/sandbox knobs outside the
4225
4295
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4226
4296
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4227
- * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
4228
- * agent-run commands to OS containment). Antigravity applies the allow/deny
4297
+ * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
4298
+ * agent-run commands to OS containment), `artifactReviewPolicy` (whether the
4299
+ * agent's artifact changes are gated on a review prompt — `asks-for-review`
4300
+ * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4301
+ * (a boolean, off by default, letting the agent read or write files outside the
4302
+ * active workspace roots). Antigravity applies the allow/deny
4229
4303
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4230
4304
  * authors these keys verbatim — no precedence modeling is needed on our side.
4231
4305
  * Fields placed here are merged onto the top level of
4232
4306
  * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
4233
4307
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4234
4308
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4235
- * Verified against https://antigravity.google/docs/cli/reference and
4236
- * https://antigravity.google/docs/cli/sandbox.
4309
+ * Verified against https://antigravity.google/docs/cli/reference,
4310
+ * https://antigravity.google/docs/cli/sandbox and
4311
+ * https://antigravity.google/docs/cli/settings.
4237
4312
  *
4238
4313
  * @example
4239
4314
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
@@ -4246,7 +4321,13 @@ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
4246
4321
  "always-proceed",
4247
4322
  "strict"
4248
4323
  ])),
4249
- enableTerminalSandbox: z.optional(z.boolean())
4324
+ enableTerminalSandbox: z.optional(z.boolean()),
4325
+ artifactReviewPolicy: z.optional(z.enum([
4326
+ "asks-for-review",
4327
+ "agent-decides",
4328
+ "always-proceed"
4329
+ ])),
4330
+ allowNonWorkspaceAccess: z.optional(z.boolean())
4250
4331
  });
4251
4332
  /**
4252
4333
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -4650,11 +4731,14 @@ const RulesyncRuleFrontmatterSchema = z.object({
4650
4731
  description: z.optional(z.string()),
4651
4732
  globs: z.optional(z.array(z.string()))
4652
4733
  })),
4653
- copilot: z.optional(z.looseObject({ excludeAgent: z.optional(z.union([
4654
- z.literal("code-review"),
4655
- z.literal("cloud-agent"),
4656
- z.literal("coding-agent")
4657
- ])) })),
4734
+ copilot: z.optional(z.looseObject({
4735
+ excludeAgent: z.optional(z.union([
4736
+ z.literal("code-review"),
4737
+ z.literal("cloud-agent"),
4738
+ z.literal("coding-agent")
4739
+ ])),
4740
+ name: z.optional(z.string())
4741
+ })),
4658
4742
  antigravity: z.optional(z.looseObject({
4659
4743
  trigger: z.optional(z.string()),
4660
4744
  globs: z.optional(z.array(z.string()))
@@ -4862,6 +4946,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
4862
4946
  arguments: z.optional(z.union([z.string(), z.array(z.string())])),
4863
4947
  context: z.optional(z.string()),
4864
4948
  agent: z.optional(z.string()),
4949
+ background: z.optional(z.boolean()),
4865
4950
  hooks: z.optional(z.looseObject({})),
4866
4951
  shell: z.optional(z.string()),
4867
4952
  "disable-model-invocation": z.optional(z.boolean()),
@@ -4913,7 +4998,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
4913
4998
  copilotcli: z.optional(z.looseObject({
4914
4999
  license: z.optional(z.string()),
4915
5000
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
4916
- "argument-hint": z.optional(z.string())
5001
+ "argument-hint": z.optional(z.string()),
5002
+ "user-invocable": z.optional(z.boolean()),
5003
+ "disable-model-invocation": z.optional(z.boolean())
4917
5004
  })),
4918
5005
  pi: z.optional(z.looseObject({
4919
5006
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
@@ -5199,6 +5286,18 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
5199
5286
  return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
5200
5287
  }
5201
5288
  //#endregion
5289
+ //#region src/constants/cursor-paths.ts
5290
+ const CURSOR_DIR = ".cursor";
5291
+ const CURSOR_COMMANDS_DIR_PATH = join(CURSOR_DIR, "commands");
5292
+ const CURSOR_SKILLS_DIR_PATH = join(CURSOR_DIR, "skills");
5293
+ const CURSOR_AGENTS_DIR_PATH = join(CURSOR_DIR, "agents");
5294
+ const CURSOR_BUGBOT_FILE_NAME = "BUGBOT.md";
5295
+ const CURSOR_MCP_FILE_NAME = "mcp.json";
5296
+ const CURSOR_HOOKS_FILE_NAME = "hooks.json";
5297
+ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5298
+ const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5299
+ const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5300
+ //#endregion
5202
5301
  //#region src/constants/takt-paths.ts
5203
5302
  const TAKT_DIR = ".takt";
5204
5303
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5649,6 +5748,235 @@ var AmpCheck = class AmpCheck extends ToolCheck {
5649
5748
  }
5650
5749
  };
5651
5750
  //#endregion
5751
+ //#region src/features/checks/check-slug.ts
5752
+ /**
5753
+ * Turn a name that came out of a tool's own config file into one safe to use as
5754
+ * a `.rulesync/checks/<name>.md` file name. Shared by the adapters whose checks
5755
+ * collapse into a single file, since the name they read back is whatever the
5756
+ * user wrote there.
5757
+ */
5758
+ function slugifyCheckName(value) {
5759
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5760
+ }
5761
+ //#endregion
5762
+ //#region src/features/checks/cursor-check.ts
5763
+ /**
5764
+ * Marks where one check starts inside the single instruction file. Bugbot reads
5765
+ * the file as prose, and an HTML comment is invisible in rendered Markdown, so
5766
+ * the marker carries the check identity without changing what Bugbot is told.
5767
+ */
5768
+ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5769
+ /**
5770
+ * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
5771
+ * code block, say. Emitting it verbatim would split that check in two on the
5772
+ * next import, so `literal-` is inserted before `check:` on the way out and
5773
+ * taken off on the way back. `(?:literal-)*` makes it a ladder, so a body that
5774
+ * already contains an escaped marker survives the round trip too.
5775
+ */
5776
+ const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5777
+ const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5778
+ const FALLBACK_CHECK_NAME = "bugbot";
5779
+ function renderMarker(name) {
5780
+ return `<!-- rulesync:check:${name} -->`;
5781
+ }
5782
+ function escapeMarkers(content) {
5783
+ return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5784
+ }
5785
+ function unescapeMarkers(content) {
5786
+ return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5787
+ }
5788
+ function findMarkers(fileContent) {
5789
+ CHECK_MARKER_PATTERN.lastIndex = 0;
5790
+ const markers = [];
5791
+ let match = CHECK_MARKER_PATTERN.exec(fileContent);
5792
+ while (match !== null) {
5793
+ markers.push({
5794
+ name: match[1] ?? "",
5795
+ start: match.index,
5796
+ end: match.index + match[0].length
5797
+ });
5798
+ match = CHECK_MARKER_PATTERN.exec(fileContent);
5799
+ }
5800
+ return markers;
5801
+ }
5802
+ /**
5803
+ * The instruction text one check contributes. Bugbot has no field to put a
5804
+ * summary in, so `description` is used only when there is no body — the same
5805
+ * fallback the file-stem heading above it gets.
5806
+ */
5807
+ function toInstruction(rulesyncCheck) {
5808
+ const body = rulesyncCheck.getBody().trim();
5809
+ if (body.length > 0) return body;
5810
+ return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5811
+ }
5812
+ function renderSection(rulesyncCheck) {
5813
+ const name = basename(rulesyncCheck.getRelativeFilePath(), ".md");
5814
+ const heading = `## ${name}`;
5815
+ const instruction = toInstruction(rulesyncCheck);
5816
+ const lines = [renderMarker(name), heading];
5817
+ if (instruction.length > 0) lines.push("", escapeMarkers(instruction));
5818
+ return lines.join("\n");
5819
+ }
5820
+ /** Drop the heading generate writes, so a round trip does not stack headings. */
5821
+ function stripGeneratedHeading(section, name) {
5822
+ const [firstLine, ...rest] = section.split("\n");
5823
+ if (firstLine?.trim() === `## ${name}`) return rest.join("\n").trim();
5824
+ return section.trim();
5825
+ }
5826
+ /**
5827
+ * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5828
+ *
5829
+ * Bugbot takes one aggregated instruction file per directory rather than a file
5830
+ * per check, so every `.rulesync/checks/*.md` targeting Cursor collapses into
5831
+ * the repository-root `.cursor/BUGBOT.md` — hence {@link fromRulesyncChecks}
5832
+ * rather than the usual per-check conversion. Each check becomes one section:
5833
+ * an HTML-comment marker carrying the check name, an `## <name>` heading, and
5834
+ * the check body as the instruction text (the `description` is used when the
5835
+ * body is empty).
5836
+ *
5837
+ * Bugbot reads the file as free prose, so a check's `severity` and `tools` have
5838
+ * no equivalent there: they are not written and do not come back on import. So
5839
+ * is `description` whenever the check also has a body.
5840
+ *
5841
+ * Project scope only — Bugbot reads repository files, and there is no
5842
+ * user-level instruction file. Bugbot also merges nested `<dir>/.cursor/BUGBOT.md`
5843
+ * files found while traversing upward from changed files, but rulesync check
5844
+ * sources carry no directory-placement semantics, so only the root file is
5845
+ * generated.
5846
+ *
5847
+ * On import the markers split the file back into one check per section; content
5848
+ * before the first marker — and a hand-written file with no markers at all —
5849
+ * becomes a single `bugbot` check, so nothing in the file is dropped. A file
5850
+ * holding anything rulesync did not write is never deleted either (see
5851
+ * {@link canDeleteAuxiliaryFiles}), though generating checks for Cursor does
5852
+ * replace it — import first to keep what is there, which is warned about.
5853
+ *
5854
+ * @see https://cursor.com/docs/bugbot
5855
+ */
5856
+ var CursorCheck = class CursorCheck extends ToolCheck {
5857
+ static getSettablePaths(_options = {}) {
5858
+ return {
5859
+ relativeDirPath: CURSOR_DIR,
5860
+ relativeFilePath: CURSOR_BUGBOT_FILE_NAME
5861
+ };
5862
+ }
5863
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
5864
+ return this.isTargetedByRulesyncCheckDefault({
5865
+ rulesyncCheck,
5866
+ toolTarget: "cursor"
5867
+ });
5868
+ }
5869
+ /**
5870
+ * Ownership guard the processor consults before it deletes anything for this
5871
+ * tool. `.cursor/BUGBOT.md` is a file Cursor's own documentation tells users
5872
+ * to hand-write, so anything in it that rulesync did not write is not
5873
+ * rulesync's to remove — dropping the last check targeting Cursor must not
5874
+ * take somebody's hand-written review instructions with it. Deletion is
5875
+ * therefore allowed only for a file that is nothing but generated sections:
5876
+ * one that carries no marker at all, or that carries hand-written text ahead
5877
+ * of the first marker, stays.
5878
+ */
5879
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
5880
+ const paths = CursorCheck.getSettablePaths();
5881
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
5882
+ if (fileContent === null) return true;
5883
+ const firstMarkerStart = findMarkers(fileContent)[0]?.start;
5884
+ if (firstMarkerStart === void 0) return false;
5885
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5886
+ }
5887
+ static fromRulesyncCheck(_params) {
5888
+ throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
5889
+ }
5890
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
5891
+ if (rulesyncChecks.length === 0) return [];
5892
+ const paths = CursorCheck.getSettablePaths({ global });
5893
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5894
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
5895
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
5896
+ const firstMarkerStart = findMarkers(existingContent)[0]?.start ?? existingContent.length;
5897
+ 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.`);
5898
+ const fileContent = `${rulesyncChecks.map(renderSection).join("\n\n")}\n`;
5899
+ return [new CursorCheck({
5900
+ outputRoot,
5901
+ relativeDirPath: paths.relativeDirPath,
5902
+ relativeFilePath,
5903
+ fileContent,
5904
+ global
5905
+ })];
5906
+ }
5907
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
5908
+ const paths = CursorCheck.getSettablePaths({ global });
5909
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5910
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
5911
+ return new CursorCheck({
5912
+ outputRoot,
5913
+ relativeDirPath: paths.relativeDirPath,
5914
+ relativeFilePath,
5915
+ fileContent: await readFileContentOrNull(filePath) ?? "",
5916
+ global
5917
+ });
5918
+ }
5919
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
5920
+ return new CursorCheck({
5921
+ outputRoot,
5922
+ relativeDirPath,
5923
+ relativeFilePath,
5924
+ fileContent: "",
5925
+ validate: false,
5926
+ global
5927
+ });
5928
+ }
5929
+ validate() {
5930
+ return {
5931
+ success: true,
5932
+ error: null
5933
+ };
5934
+ }
5935
+ toRulesyncCheck() {
5936
+ const first = this.toRulesyncChecks()[0];
5937
+ if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
5938
+ return first;
5939
+ }
5940
+ toRulesyncChecks() {
5941
+ const fileContent = this.getFileContent();
5942
+ const sections = [];
5943
+ const markers = findMarkers(fileContent);
5944
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5945
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5946
+ if (preamble.length > 0) sections.push({
5947
+ name: FALLBACK_CHECK_NAME,
5948
+ content: unescapeMarkers(preamble)
5949
+ });
5950
+ for (const [index, marker] of markers.entries()) {
5951
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5952
+ const markerName = marker.name.trim();
5953
+ const name = slugifyCheckName(markerName) || FALLBACK_CHECK_NAME;
5954
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5955
+ sections.push({
5956
+ name,
5957
+ content: unescapeMarkers(content)
5958
+ });
5959
+ }
5960
+ const used = /* @__PURE__ */ new Set();
5961
+ return sections.map(({ name, content }) => {
5962
+ let uniqueName = name;
5963
+ let suffix = 2;
5964
+ while (used.has(uniqueName)) {
5965
+ uniqueName = `${name}-${suffix}`;
5966
+ suffix += 1;
5967
+ }
5968
+ used.add(uniqueName);
5969
+ return new RulesyncCheck({
5970
+ outputRoot: ".",
5971
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5972
+ relativeFilePath: `${uniqueName}.md`,
5973
+ frontmatter: { targets: ["*"] },
5974
+ body: content
5975
+ });
5976
+ });
5977
+ }
5978
+ };
5979
+ //#endregion
5652
5980
  //#region src/constants/hermesagent-paths.ts
5653
5981
  /**
5654
5982
  * Hermes Agent configuration-layout conventions.
@@ -6147,7 +6475,20 @@ const SHARED_CONFIG_OWNERSHIP = {
6147
6475
  jsoncParseErrors: "error",
6148
6476
  features: { permissions: {
6149
6477
  kind: "replace-owned-keys",
6150
- ownedKeys: ["chat.tools.terminal.autoApprove"]
6478
+ ownedKeys: [
6479
+ "chat.tools.terminal.autoApprove",
6480
+ "chat.tools.edits.autoApprove",
6481
+ "chat.tools.urls.autoApprove"
6482
+ ]
6483
+ } }
6484
+ },
6485
+ ".vscode/mcp.json": {
6486
+ format: "jsonc",
6487
+ invalidRootPolicy: "error",
6488
+ jsoncParseErrors: "error",
6489
+ features: { mcp: {
6490
+ kind: "replace-owned-keys",
6491
+ ownedKeys: ["servers"]
6151
6492
  } }
6152
6493
  },
6153
6494
  ".qwen/settings.json": {
@@ -6513,7 +6854,12 @@ const applyIgnoreReadDenies = (params) => {
6513
6854
  const applyPermissions = (params) => {
6514
6855
  const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
6515
6856
  const current = parsePermissionsBlock(settings);
6516
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
6857
+ const emitted = /* @__PURE__ */ new Set([
6858
+ ...allow,
6859
+ ...ask,
6860
+ ...deny
6861
+ ]);
6862
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)) && !emitted.has(entry));
6517
6863
  if (logger && managedToolNames.has(READ_TOOL_NAME)) {
6518
6864
  const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
6519
6865
  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.`);
@@ -6663,12 +7009,9 @@ function toRulesyncCheckFromGate({ gate, index, scope, editOnly }) {
6663
7009
  body: ""
6664
7010
  });
6665
7011
  }
6666
- function slugify(value) {
6667
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
6668
- }
6669
7012
  function slugForGate({ gate, index, scope }) {
6670
- const slug = slugify(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
6671
- return `${scope ? `${slugify(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
7013
+ const slug = slugifyCheckName(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
7014
+ return `${scope ? `${slugifyCheckName(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
6672
7015
  }
6673
7016
  /**
6674
7017
  * Checks adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -6844,6 +7187,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
6844
7187
  filePattern: "*.md"
6845
7188
  }
6846
7189
  }],
7190
+ ["cursor", {
7191
+ class: CursorCheck,
7192
+ meta: {
7193
+ supportsGlobal: false,
7194
+ filePattern: CURSOR_BUGBOT_FILE_NAME
7195
+ }
7196
+ }],
6847
7197
  ["hermesagent", {
6848
7198
  class: HermesagentCheck,
6849
7199
  meta: {
@@ -7259,6 +7609,7 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
7259
7609
  const ANTIGRAVITY_DIR = ".agents";
7260
7610
  const ANTIGRAVITY_SKILLS_DIR_PATH = join(ANTIGRAVITY_DIR, "skills");
7261
7611
  const ANTIGRAVITY_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_DIR, "workflows");
7612
+ const ANTIGRAVITY_AGENTS_DIR_PATH = join(ANTIGRAVITY_DIR, "agents");
7262
7613
  const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
7263
7614
  const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
7264
7615
  const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
@@ -7269,6 +7620,7 @@ const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
7269
7620
  const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
7270
7621
  const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
7271
7622
  const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
7623
+ const ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH = join(ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH, "agents");
7272
7624
  //#endregion
7273
7625
  //#region src/constants/antigravity-cli-paths.ts
7274
7626
  const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
@@ -7827,6 +8179,7 @@ const CLAUDECODE_PLUGIN_HOOKS_DIR = "hooks";
7827
8179
  const CLAUDECODE_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7828
8180
  const ANTIGRAVITY_PLUGIN_RULES_DIR = "rules";
7829
8181
  const ANTIGRAVITY_PLUGIN_SKILLS_DIR = "skills";
8182
+ const ANTIGRAVITY_PLUGIN_AGENTS_DIR = "agents";
7830
8183
  const ANTIGRAVITY_PLUGIN_MCP_FILE_NAME = "mcp_config.json";
7831
8184
  const ANTIGRAVITY_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7832
8185
  //#endregion
@@ -8051,6 +8404,7 @@ const COPILOT_SKILLS_DIR_PATH = join(GITHUB_DIR, "skills");
8051
8404
  const COPILOT_AGENTS_DIR_PATH = join(GITHUB_DIR, "agents");
8052
8405
  const COPILOT_HOOKS_DIR_PATH = join(GITHUB_DIR, "hooks");
8053
8406
  const COPILOT_HOOKS_FILE_NAME = "copilot-hooks.json";
8407
+ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
8054
8408
  const COPILOT_MCP_DIR = ".vscode";
8055
8409
  const COPILOT_MCP_FILE_NAME = "mcp.json";
8056
8410
  const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
@@ -8058,6 +8412,7 @@ const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
8058
8412
  const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
8059
8413
  const COPILOTCLI_AGENTS_DIR_PATH = join(COPILOT_DIR, "agents");
8060
8414
  const COPILOTCLI_HOOKS_DIR_PATH = join(COPILOT_DIR, "hooks");
8415
+ const COPILOT_GLOBAL_HOOKS_DIR_PATH = COPILOTCLI_HOOKS_DIR_PATH;
8061
8416
  const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
8062
8417
  const COPILOT_SKILLS_GLOBAL_DIR_PATH = join(COPILOT_DIR, "skills");
8063
8418
  //#endregion
@@ -8181,17 +8536,6 @@ var CopilotCommand = class CopilotCommand extends ToolCommand {
8181
8536
  }
8182
8537
  };
8183
8538
  //#endregion
8184
- //#region src/constants/cursor-paths.ts
8185
- const CURSOR_DIR = ".cursor";
8186
- const CURSOR_COMMANDS_DIR_PATH = join(CURSOR_DIR, "commands");
8187
- const CURSOR_SKILLS_DIR_PATH = join(CURSOR_DIR, "skills");
8188
- const CURSOR_AGENTS_DIR_PATH = join(CURSOR_DIR, "agents");
8189
- const CURSOR_MCP_FILE_NAME = "mcp.json";
8190
- const CURSOR_HOOKS_FILE_NAME = "hooks.json";
8191
- const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
8192
- const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
8193
- const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
8194
- //#endregion
8195
8539
  //#region src/features/commands/cursor-command.ts
8196
8540
  const CursorCommandFrontmatterSchema = z.looseObject({
8197
8541
  description: z.optional(z.string()),
@@ -11960,6 +12304,13 @@ function groupDefinitionsByMatcher(definitions) {
11960
12304
  }
11961
12305
  return byMatcher;
11962
12306
  }
12307
+ /** `$CLAUDE_PROJECT_DIR` -> `${CLAUDE_PROJECT_DIR}`, the form the tool substitutes. */
12308
+ function bracePlaceholder(projectDirVar) {
12309
+ return `\${${projectDirVar.replace(/^\$/, "")}}`;
12310
+ }
12311
+ function stripSurroundingQuotes(value) {
12312
+ return value.replace(/^(["'])(.*)\1$/, "$2").replace(/^["']/, "");
12313
+ }
11963
12314
  /**
11964
12315
  * Apply the optional project directory variable prefix to a command string.
11965
12316
  */
@@ -11969,8 +12320,10 @@ function applyCommandPrefix({ def, converterConfig }) {
11969
12320
  const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
11970
12321
  const isDotRelativeCommand = unquotedCommand?.startsWith(".") ?? false;
11971
12322
  const isAbsoluteCommand = typeof unquotedCommand === "string" && (posix.isAbsolute(unquotedCommand) || win32.isAbsolute(unquotedCommand) || unquotedCommand.startsWith("~/"));
12323
+ const isExecForm = (converterConfig.arrayPassthroughFields?.some(({ canonical }) => canonical === "args") ?? false) && Array.isArray(def.args);
11972
12324
  if (!(converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && !isAbsoluteCommand && (!converterConfig.prefixDotRelativeCommandsOnly || isDotRelativeCommand)) || typeof trimmedCommand !== "string") return def.command;
11973
12325
  const relativeCommand = trimmedCommand.replace(/^(["'])\.\//, "$1").replace(/^\.\//, "");
12326
+ if (isExecForm) return `${bracePlaceholder(converterConfig.projectDirVar)}/${stripSurroundingQuotes(relativeCommand)}`;
11974
12327
  return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
11975
12328
  }
11976
12329
  /**
@@ -11978,8 +12331,11 @@ function applyCommandPrefix({ def, converterConfig }) {
11978
12331
  * canonical field name to its (possibly renamed) tool field name. Only boolean
11979
12332
  * values are carried through.
11980
12333
  */
11981
- function emitBooleanPassthroughFields({ def, converterConfig }) {
11982
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "boolean").map(({ canonical, tool }) => [tool, def[canonical]]));
12334
+ function emitBooleanPassthroughFields({ def, hookType, converterConfig }) {
12335
+ return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12336
+ if (commandOnly === true && hookType !== "command") return false;
12337
+ return typeof def[canonical] === "boolean";
12338
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
11983
12339
  }
11984
12340
  /**
11985
12341
  * Import the configured boolean passthrough fields back into canonical fields,
@@ -11993,8 +12349,11 @@ function importBooleanPassthroughFields({ h, converterConfig }) {
11993
12349
  * canonical field name to its (possibly renamed) tool field name. Only non-empty
11994
12350
  * string values are carried through.
11995
12351
  */
11996
- function emitStringPassthroughFields({ def, converterConfig }) {
11997
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "string" && def[canonical] !== "").map(({ canonical, tool }) => [tool, def[canonical]]));
12352
+ function emitStringPassthroughFields({ def, hookType, converterConfig }) {
12353
+ return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12354
+ if (commandOnly === true && hookType !== "command") return false;
12355
+ return typeof def[canonical] === "string" && def[canonical] !== "";
12356
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
11998
12357
  }
11999
12358
  /**
12000
12359
  * Import the configured string passthrough fields back into canonical fields,
@@ -12007,8 +12366,11 @@ function importStringPassthroughFields({ h, converterConfig }) {
12007
12366
  /**
12008
12367
  * Emit the configured string-array passthrough fields on the tool side.
12009
12368
  */
12010
- function emitArrayPassthroughFields({ def, converterConfig }) {
12011
- return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical }) => isStringArray(def[canonical])).map(({ canonical, tool }) => [tool, def[canonical]]));
12369
+ function emitArrayPassthroughFields({ def, hookType, converterConfig }) {
12370
+ return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12371
+ if (commandOnly === true && hookType !== "command") return false;
12372
+ return isStringArray(def[canonical]);
12373
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12012
12374
  }
12013
12375
  /**
12014
12376
  * Import the configured string-array passthrough fields, reversing
@@ -12085,14 +12447,17 @@ function buildToolHooks({ defs, converterConfig }) {
12085
12447
  hooks.push({
12086
12448
  ...emitBooleanPassthroughFields({
12087
12449
  def,
12450
+ hookType,
12088
12451
  converterConfig
12089
12452
  }),
12090
12453
  ...emitStringPassthroughFields({
12091
12454
  def,
12455
+ hookType,
12092
12456
  converterConfig
12093
12457
  }),
12094
12458
  ...emitArrayPassthroughFields({
12095
12459
  def,
12460
+ hookType,
12096
12461
  converterConfig
12097
12462
  }),
12098
12463
  type: hookType,
@@ -12176,6 +12541,8 @@ function stripCommandPrefix({ command, converterConfig }) {
12176
12541
  if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
12177
12542
  const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
12178
12543
  if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
12544
+ const bracedPrefix = `${bracePlaceholder(converterConfig.projectDirVar)}/`;
12545
+ if (cmd.startsWith(bracedPrefix)) return `./${cmd.slice(bracedPrefix.length)}`;
12179
12546
  if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
12180
12547
  const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12181
12548
  return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
@@ -12723,7 +13090,10 @@ const CLAUDE_CONVERTER_CONFIG = {
12723
13090
  "taskCreated",
12724
13091
  "taskCompleted",
12725
13092
  "teammateIdle",
12726
- "cwdChanged"
13093
+ "cwdChanged",
13094
+ "beforeSubmitPrompt",
13095
+ "stop",
13096
+ "directoryAdded"
12727
13097
  ]),
12728
13098
  supportedHookTypes: /* @__PURE__ */ new Set([
12729
13099
  "command",
@@ -12733,9 +13103,45 @@ const CLAUDE_CONVERTER_CONFIG = {
12733
13103
  "agent"
12734
13104
  ]),
12735
13105
  emitsPromptModel: true,
12736
- stringPassthroughFields: [{
12737
- canonical: "if",
12738
- tool: "if"
13106
+ stringPassthroughFields: [
13107
+ {
13108
+ canonical: "if",
13109
+ tool: "if"
13110
+ },
13111
+ {
13112
+ canonical: "statusMessage",
13113
+ tool: "statusMessage"
13114
+ },
13115
+ {
13116
+ canonical: "shell",
13117
+ tool: "shell",
13118
+ commandOnly: true
13119
+ }
13120
+ ],
13121
+ booleanPassthroughFields: [
13122
+ {
13123
+ canonical: "once",
13124
+ tool: "once"
13125
+ },
13126
+ {
13127
+ canonical: "async",
13128
+ tool: "async",
13129
+ commandOnly: true
13130
+ },
13131
+ {
13132
+ canonical: "asyncRewake",
13133
+ tool: "asyncRewake",
13134
+ commandOnly: true
13135
+ },
13136
+ {
13137
+ canonical: "continueOnBlock",
13138
+ tool: "continueOnBlock"
13139
+ }
13140
+ ],
13141
+ arrayPassthroughFields: [{
13142
+ canonical: "args",
13143
+ tool: "args",
13144
+ commandOnly: true
12739
13145
  }]
12740
13146
  };
12741
13147
  var ClaudecodeHooks = class extends ToolHooks {
@@ -12843,7 +13249,14 @@ const CODEXCLI_CONVERTER_CONFIG = {
12843
13249
  toolToCanonicalEventNames: CODEXCLI_TO_CANONICAL_EVENT_NAMES,
12844
13250
  projectDirVar: "",
12845
13251
  supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
12846
- passthroughFields: ["name", "description"]
13252
+ passthroughFields: ["name", "description"],
13253
+ stringPassthroughFields: [{
13254
+ canonical: "commandWindows",
13255
+ tool: "commandWindows"
13256
+ }, {
13257
+ canonical: "statusMessage",
13258
+ tool: "statusMessage"
13259
+ }]
12847
13260
  };
12848
13261
  /**
12849
13262
  * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
@@ -13091,7 +13504,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13091
13504
  fileContent: params.fileContent ?? "{}"
13092
13505
  });
13093
13506
  }
13094
- static getSettablePaths(_options = {}) {
13507
+ static getSettablePaths({ global = false } = {}) {
13508
+ if (global) return {
13509
+ relativeDirPath: COPILOT_GLOBAL_HOOKS_DIR_PATH,
13510
+ relativeFilePath: COPILOT_GLOBAL_HOOKS_FILE_NAME
13511
+ };
13095
13512
  return {
13096
13513
  relativeDirPath: COPILOT_HOOKS_DIR_PATH,
13097
13514
  relativeFilePath: COPILOT_HOOKS_FILE_NAME
@@ -13105,11 +13522,12 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13105
13522
  relativeDirPath: paths.relativeDirPath,
13106
13523
  relativeFilePath: paths.relativeFilePath,
13107
13524
  fileContent,
13108
- validate
13525
+ validate,
13526
+ global
13109
13527
  });
13110
13528
  }
13111
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true }) {
13112
- const paths = CopilotHooks.getSettablePaths();
13529
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
13530
+ const paths = CopilotHooks.getSettablePaths({ global });
13113
13531
  const copilotHooks = canonicalToCopilotHooks(rulesyncHooks.getJson());
13114
13532
  const fileContent = JSON.stringify({
13115
13533
  version: 1,
@@ -13120,7 +13538,8 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13120
13538
  relativeDirPath: paths.relativeDirPath,
13121
13539
  relativeFilePath: paths.relativeFilePath,
13122
13540
  fileContent,
13123
- validate
13541
+ validate,
13542
+ global
13124
13543
  });
13125
13544
  }
13126
13545
  toRulesyncHooks(options) {
@@ -13164,10 +13583,16 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13164
13583
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
13165
13584
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
13166
13585
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
13167
- * `notification`, `preMcpToolCall`). Each entry supports three hook types:
13586
+ * `notification`, `userPromptTransformed`, `preMcpToolCall`). Each entry
13587
+ * supports three hook types:
13168
13588
  *
13169
13589
  * - `command` — the `bash` / `powershell` command-field shape with optional
13170
- * `timeoutSec`, plus optional `cwd` / `env`.
13590
+ * `timeoutSec`, plus optional `cwd` / `env`. Upstream also accepts the
13591
+ * portable `command` field (copied to both shells when neither is present)
13592
+ * and `timeout` as an alias for `timeoutSec`; rulesync reads both on import.
13593
+ * On generate the canonical `shell` selector picks `bash` or `powershell`,
13594
+ * and without it the portable `command` field is written — so the generated
13595
+ * file does not depend on the machine rulesync ran on.
13171
13596
  * - `prompt` — a `prompt` string (Copilot CLI only honors prompt hooks on
13172
13597
  * `sessionStart`, so prompt hooks on other events are skipped).
13173
13598
  * - `http` — `url` / `headers` / `allowedEnvVars` with optional `timeoutSec`.
@@ -13193,11 +13618,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13193
13618
  * all rulesync-managed Copilot CLI files under the single `~/.copilot/`
13194
13619
  * root and will revisit if the spec later mandates an alternate layout.
13195
13620
  *
13196
- * Hook entries on `preToolUse` / `postToolUse` may carry an optional `matcher`
13197
- * field (a regex compiled as `^(?:PATTERN)$`, tested against `toolName`); it is
13198
- * emitted on those events and dropped (with a warning) on any other event,
13199
- * which never honors matchers. See changelog v1.0.36 (2026-04-24) and v1.0.63
13200
- * (2026-06-15). Reference: https://docs.github.com/en/copilot/reference/hooks-reference
13621
+ * Hook entries on the six matcher-aware events (see
13622
+ * {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
13623
+ * is emitted on those events and dropped (with a warning) on any other event,
13624
+ * which never honors matchers.
13625
+ * Reference: https://docs.github.com/en/copilot/reference/hooks-reference
13201
13626
  *
13202
13627
  * The output JSON schema and platform-specific `bash` / `powershell` command
13203
13628
  * field selection match `copilot-hooks.ts`, but the event surface diverges (the
@@ -13212,19 +13637,30 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13212
13637
  * `Edit|Write`) are now honored instead of silently dropped").
13213
13638
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
13214
13639
  */
13215
- const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
13640
+ const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set([
13641
+ "preToolUse",
13642
+ "postToolUse",
13643
+ "notification",
13644
+ "permissionRequest",
13645
+ "preCompact",
13646
+ "subagentStart"
13647
+ ]);
13648
+ /** Human-readable list of the matcher-aware events, for the drop warning. */
13649
+ const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
13216
13650
  const CopilotCliHookEntrySchema = z.looseObject({
13217
13651
  type: z._default(z.string(), "command"),
13218
13652
  matcher: z.optional(z.string()),
13219
13653
  bash: z.optional(z.string()),
13220
13654
  powershell: z.optional(z.string()),
13655
+ command: z.optional(z.string()),
13221
13656
  prompt: z.optional(z.string()),
13222
13657
  url: z.optional(z.string()),
13223
13658
  headers: z.optional(z.record(z.string(), z.string())),
13224
13659
  allowedEnvVars: z.optional(z.array(z.string())),
13225
13660
  cwd: z.optional(z.string()),
13226
13661
  env: z.optional(z.record(z.string(), z.string())),
13227
- timeoutSec: z.optional(z.number())
13662
+ timeoutSec: z.optional(z.number()),
13663
+ timeout: z.optional(z.number())
13228
13664
  });
13229
13665
  /** Filter the shared config hooks down to events the Copilot CLI supports. */
13230
13666
  function filterSupportedCopilotCliHooks(hooks) {
@@ -13235,21 +13671,21 @@ function filterSupportedCopilotCliHooks(hooks) {
13235
13671
  }
13236
13672
  /**
13237
13673
  * Resolve the `matcher` part for an exported entry. Copilot CLI honors `matcher`
13238
- * only on preToolUse/postToolUse entries; on any other event a matcher would be
13239
- * silently dropped by the CLI, so we drop it here with a warning rather than
13240
- * emitting a dead field.
13674
+ * only on the events listed in {@link COPILOTCLI_MATCHER_EVENTS}; on any other
13675
+ * event a matcher would be silently dropped by the CLI, so we drop it here with
13676
+ * a warning rather than emitting a dead field.
13241
13677
  */
13242
13678
  function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger }) {
13243
13679
  if (matcher === void 0 || matcher === null || matcher === "") return {};
13244
13680
  if (matcherSupported) return { matcher };
13245
- logger?.warn(`Copilot CLI hook matchers are only honored on preToolUse/postToolUse; dropping matcher "${matcher}" on '${eventName}'.`);
13681
+ logger?.warn(`Copilot CLI hook matchers are only honored on ${COPILOTCLI_MATCHER_EVENTS_LABEL}; dropping matcher "${matcher}" on '${eventName}'.`);
13246
13682
  return {};
13247
13683
  }
13248
13684
  /**
13249
13685
  * Build the exported entries for a single canonical event. Returns an empty
13250
13686
  * array when no entries are emitted (e.g. all prompt hooks were skipped).
13251
13687
  */
13252
- function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, commandField, logger }) {
13688
+ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, logger }) {
13253
13689
  const matcherSupported = COPILOTCLI_MATCHER_EVENTS.has(eventName);
13254
13690
  const entries = [];
13255
13691
  for (const def of definitions) {
@@ -13285,22 +13721,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
13285
13721
  ...timeoutPart,
13286
13722
  ...rest
13287
13723
  });
13288
- else if (hookType === "command") entries.push({
13289
- type: "command",
13290
- ...matcherPart,
13291
- ...compact({
13292
- [commandField]: def.command,
13293
- env: def.env
13294
- }),
13295
- ...timeoutPart,
13296
- ...rest
13297
- });
13724
+ else if (hookType === "command") {
13725
+ const commandField = def.shell ?? "command";
13726
+ entries.push({
13727
+ type: "command",
13728
+ ...matcherPart,
13729
+ ...compact({
13730
+ [commandField]: def.command,
13731
+ env: def.env
13732
+ }),
13733
+ ...timeoutPart,
13734
+ ...rest
13735
+ });
13736
+ }
13298
13737
  }
13299
13738
  return entries;
13300
13739
  }
13301
13740
  function canonicalToCopilotCliHooks(config, logger) {
13302
13741
  const canonicalSchemaKeys = Object.keys(HookDefinitionSchema.shape);
13303
- const commandField = process.platform === "win32" ? "powershell" : "bash";
13304
13742
  const effectiveHooks = {
13305
13743
  ...filterSupportedCopilotCliHooks(config.hooks),
13306
13744
  ...config.copilot?.hooks,
@@ -13313,7 +13751,6 @@ function canonicalToCopilotCliHooks(config, logger) {
13313
13751
  eventName,
13314
13752
  definitions,
13315
13753
  canonicalSchemaKeys,
13316
- commandField,
13317
13754
  logger
13318
13755
  });
13319
13756
  if (entries.length > 0) out[copilotEventName] = entries;
@@ -13330,6 +13767,13 @@ function importPassthrough(entry) {
13330
13767
  if (entry.env !== void 0) passthrough.env = entry.env;
13331
13768
  return passthrough;
13332
13769
  }
13770
+ /**
13771
+ * Resolve the canonical command and its `shell` selector from an imported entry.
13772
+ *
13773
+ * A shell-specific field carries its `shell` through so re-export writes the
13774
+ * same field back. An entry using only the portable `command` field leaves
13775
+ * `shell` unset, which re-export renders as the portable field again.
13776
+ */
13333
13777
  function resolveImportCommand(entry, logger) {
13334
13778
  const hasBash = typeof entry.bash === "string";
13335
13779
  const hasPowershell = typeof entry.powershell === "string";
@@ -13338,9 +13782,22 @@ function resolveImportCommand(entry, logger) {
13338
13782
  const chosen = isWindows ? "powershell" : "bash";
13339
13783
  const ignored = isWindows ? "bash" : "powershell";
13340
13784
  logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
13341
- return isWindows ? entry.powershell : entry.bash;
13342
- } else if (hasBash) return entry.bash;
13343
- else if (hasPowershell) return entry.powershell;
13785
+ return isWindows ? {
13786
+ command: entry.powershell,
13787
+ shell: "powershell"
13788
+ } : {
13789
+ command: entry.bash,
13790
+ shell: "bash"
13791
+ };
13792
+ } else if (hasBash) return {
13793
+ command: entry.bash,
13794
+ shell: "bash"
13795
+ };
13796
+ else if (hasPowershell) return {
13797
+ command: entry.powershell,
13798
+ shell: "powershell"
13799
+ };
13800
+ return typeof entry.command === "string" ? { command: entry.command } : {};
13344
13801
  }
13345
13802
  function copilotCliHooksToCanonical(rawHooks, logger) {
13346
13803
  if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
@@ -13353,7 +13810,7 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13353
13810
  const parseResult = CopilotCliHookEntrySchema.safeParse(rawEntry);
13354
13811
  if (!parseResult.success) continue;
13355
13812
  const entry = parseResult.data;
13356
- const timeout = entry.timeoutSec;
13813
+ const timeout = entry.timeoutSec ?? entry.timeout;
13357
13814
  const timeoutPart = timeout !== void 0 ? { timeout } : {};
13358
13815
  const matcherPart = entry.matcher !== void 0 && entry.matcher !== "" ? { matcher: entry.matcher } : {};
13359
13816
  const passthrough = importPassthrough(entry);
@@ -13372,10 +13829,11 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13372
13829
  ...passthrough
13373
13830
  });
13374
13831
  else {
13375
- const command = resolveImportCommand(entry, logger);
13832
+ const { command, shell } = resolveImportCommand(entry, logger);
13376
13833
  defs.push({
13377
13834
  type: "command",
13378
13835
  ...command !== void 0 && { command },
13836
+ ...shell !== void 0 && { shell },
13379
13837
  ...matcherPart,
13380
13838
  ...timeoutPart,
13381
13839
  ...passthrough
@@ -14515,7 +14973,23 @@ var JunieHooks = class JunieHooks extends ToolHooks {
14515
14973
  };
14516
14974
  //#endregion
14517
14975
  //#region src/features/hooks/opencode-style-generator.ts
14518
- const NAMED_HOOKS = /* @__PURE__ */ new Set(["tool.execute.before", "tool.execute.after"]);
14976
+ /**
14977
+ * Tool events emitted as named `(input, ...)` hooks rather than through the
14978
+ * generic `event.type` dispatch, mapped to the expression a hook's `matcher`
14979
+ * regex is tested against — or `null` when the hook has no matchable subject,
14980
+ * in which case a matcher is dropped rather than compiled against a field that
14981
+ * does not exist.
14982
+ *
14983
+ * `experimental.session.compacting` receives `(input, output)` and exposes no
14984
+ * per-invocation identifier worth matching on, so it takes `null`.
14985
+ *
14986
+ * @see https://opencode.ai/docs/plugins/
14987
+ */
14988
+ const NAMED_HOOK_MATCHER_SUBJECTS = {
14989
+ "tool.execute.before": "input.tool",
14990
+ "tool.execute.after": "input.tool",
14991
+ "experimental.session.compacting": null
14992
+ };
14519
14993
  function escapeForTemplateLiteral(command) {
14520
14994
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
14521
14995
  }
@@ -14548,7 +15022,7 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
14548
15022
  });
14549
15023
  }
14550
15024
  if (handlers.length > 0) {
14551
- const grouped = NAMED_HOOKS.has(toolEvent) ? namedEventHandlers : genericEventHandlers;
15025
+ const grouped = Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) ? namedEventHandlers : genericEventHandlers;
14552
15026
  const existing = grouped[toolEvent];
14553
15027
  if (existing) existing.push(...handlers);
14554
15028
  else grouped[toolEvent] = handlers;
@@ -14577,14 +15051,15 @@ function buildGenericEventBodyLines(genericEventHandlers) {
14577
15051
  function buildNamedEventBodyLines(namedEventHandlers) {
14578
15052
  const bodyLines = [];
14579
15053
  for (const [eventName, handlers] of Object.entries(namedEventHandlers)) {
15054
+ const matcherSubject = NAMED_HOOK_MATCHER_SUBJECTS[eventName] ?? null;
14580
15055
  bodyLines.push(` "${eventName}": async (input) => {`);
14581
15056
  for (const handler of handlers) {
14582
15057
  const escapedCommand = escapeForTemplateLiteral(handler.command);
14583
- if (handler.matcher) {
15058
+ if (handler.matcher && matcherSubject !== null) {
14584
15059
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
14585
15060
  bodyLines.push(" {");
14586
15061
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
14587
- bodyLines.push(` if (__re.test(input.tool)) {`);
15062
+ bodyLines.push(` if (__re.test(${matcherSubject})) {`);
14588
15063
  bodyLines.push(` await $\`${escapedCommand}\`;`);
14589
15064
  bodyLines.push(" }");
14590
15065
  bodyLines.push(" }");
@@ -16220,7 +16695,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
16220
16695
  class: CopilotHooks,
16221
16696
  meta: {
16222
16697
  supportsProject: true,
16223
- supportsGlobal: false,
16698
+ supportsGlobal: true,
16224
16699
  supportsImport: true
16225
16700
  },
16226
16701
  supportedEvents: COPILOT_HOOK_EVENTS,
@@ -18697,8 +19172,20 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
18697
19172
  disabledTools: "disabled_tools",
18698
19173
  envVars: "env_vars"
18699
19174
  };
19175
+ const RULESYNC_TO_CODEX_SCALAR_FIELD_MAP = { experimentalEnvironment: "experimental_environment" };
19176
+ const CODEX_TO_RULESYNC_SCALAR_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
18700
19177
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
18701
19178
  /**
19179
+ * `env_vars` entries are either a bare variable name or `{ name, source }`,
19180
+ * where `source = "remote"` reads the variable from the remote executor
19181
+ * environment. The other renamed keys (`enabled_tools`, `disabled_tools`) stay
19182
+ * plain string arrays, so the widened check applies to `env_vars` only.
19183
+ * @see https://learn.chatgpt.com/docs/extend/mcp
19184
+ */
19185
+ function isValidRenamedArray(key, value) {
19186
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
19187
+ }
19188
+ /**
18702
19189
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
18703
19190
  * Code style camelCase) into the shape Codex CLI understands. Codex expects the
18704
19191
  * OAuth client id under snake_case `client_id`; without it `codex mcp login`
@@ -18758,8 +19245,12 @@ function convertFromCodexFormat(codexMcp) {
18758
19245
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
18759
19246
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
18760
19247
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
18761
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
19248
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18762
19249
  else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
19250
+ } else if (Object.hasOwn(CODEX_TO_RULESYNC_SCALAR_FIELD_MAP, key)) {
19251
+ const mappedKey = CODEX_TO_RULESYNC_SCALAR_FIELD_MAP[key];
19252
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
19253
+ else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
18763
19254
  } else converted[key] = value;
18764
19255
  }
18765
19256
  result[name] = converted;
@@ -18781,8 +19272,12 @@ function convertToCodexFormat(mcpServers) {
18781
19272
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
18782
19273
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
18783
19274
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
18784
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
19275
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18785
19276
  else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
19277
+ } else if (Object.hasOwn(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP, key)) {
19278
+ const mappedKey = RULESYNC_TO_CODEX_SCALAR_FIELD_MAP[key];
19279
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
19280
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
18786
19281
  } else converted[key] = value;
18787
19282
  }
18788
19283
  const previousName = originalNames.get(codexName);
@@ -18853,7 +19348,9 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18853
19348
  const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
18854
19349
  return [serverName, {
18855
19350
  ...serverConfig,
18856
- ...isRecord(rawServer) && isStringArray$1(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
19351
+ ...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
19352
+ ...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
19353
+ ...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
18857
19354
  }];
18858
19355
  })));
18859
19356
  const filteredMcpServers = this.removeEmptyEntries(converted);
@@ -18924,9 +19421,6 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18924
19421
  };
18925
19422
  //#endregion
18926
19423
  //#region src/features/mcp/copilot-mcp.ts
18927
- function convertToCopilotFormat(mcpServers) {
18928
- return { servers: mcpServers };
18929
- }
18930
19424
  function convertFromCopilotFormat(copilotConfig) {
18931
19425
  return copilotConfig.servers ?? {};
18932
19426
  }
@@ -18955,13 +19449,21 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
18955
19449
  validate
18956
19450
  });
18957
19451
  }
18958
- static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
18959
- const copilotConfig = convertToCopilotFormat(rulesyncMcp.getMcpServers());
19452
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
19453
+ const paths = this.getSettablePaths();
19454
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
19455
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
18960
19456
  return new CopilotMcp({
18961
19457
  outputRoot,
18962
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18963
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18964
- fileContent: JSON.stringify(copilotConfig, null, 2),
19458
+ relativeDirPath: paths.relativeDirPath,
19459
+ relativeFilePath: paths.relativeFilePath,
19460
+ fileContent: applySharedConfigPatch({
19461
+ fileKey: sharedConfigFileKey(paths),
19462
+ feature: "mcp",
19463
+ existingContent,
19464
+ patch: { servers: rulesyncMcp.getMcpServers() },
19465
+ filePath
19466
+ }),
18965
19467
  validate
18966
19468
  });
18967
19469
  }
@@ -20644,7 +21146,7 @@ const KILO_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
20644
21146
  * impossible to start — no command, no URL — so it is written as the toggle it
20645
21147
  * resembles rather than dropped, but the loss is said out loud.
20646
21148
  */
20647
- function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
21149
+ function warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger) {
20648
21150
  const dropped = Object.keys(serverConfig).filter((key) => !KILO_TOGGLE_KEPT_KEYS.has(key));
20649
21151
  if (dropped.length === 0) return;
20650
21152
  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.`);
@@ -20657,7 +21159,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20657
21159
  if (declaresNoTransport(serverConfig)) {
20658
21160
  if (serverConfig.disabled === void 0) {
20659
21161
  if (existingEntry !== void 0 && !isKiloTransportServer(existingEntry)) {
20660
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21162
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20661
21163
  return existingEntry;
20662
21164
  }
20663
21165
  return warnAndSkipMcpServer({
@@ -20667,7 +21169,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20667
21169
  logger
20668
21170
  });
20669
21171
  }
20670
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21172
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20671
21173
  return { enabled: !serverConfig.disabled };
20672
21174
  }
20673
21175
  if (isRemoteMcpServer(serverConfig)) {
@@ -21293,13 +21795,49 @@ const OpencodeMcpRemoteServerSchema = z.looseObject({
21293
21795
  headers: z.optional(z.record(z.string(), z.string())),
21294
21796
  enabled: z._default(z.boolean(), true)
21295
21797
  });
21296
- const OpencodeMcpServerSchema = z.union([OpencodeMcpLocalServerSchema, OpencodeMcpRemoteServerSchema]);
21798
+ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21799
+ const OPENCODE_MCP_TRANSPORT_KEYS = [
21800
+ ...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
21801
+ ...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
21802
+ ...OPENCODE_PASSTHROUGH_SERVER_FIELDS
21803
+ ].filter((key) => key !== "enabled");
21804
+ /**
21805
+ * A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
21806
+ * disabling a server another config layer defines. It is the third member of
21807
+ * OpenCode's own `mcp` union in the published schema, described in-source as
21808
+ * "the legacy `{ enabled: false }` form used to disable a server". Without this
21809
+ * arm the union rejects the entry and the whole MCP import aborts — taking
21810
+ * every valid server in the same file down with it.
21811
+ *
21812
+ * Loose, unlike the two transport arms, so a key OpenCode adds to a toggle
21813
+ * later does not bring that abort back — but refined to reject anything
21814
+ * carrying a key only a transport entry has. A plain loose arm would sit under
21815
+ * a malformed `local` or `remote` entry that happens to carry `enabled` and
21816
+ * swallow it, dropping its command, URL, or headers without a word instead of
21817
+ * failing the way it does today. Mirrors {@link KiloMcpToggleSchema}.
21818
+ *
21819
+ * @see https://opencode.ai/config.json
21820
+ */
21821
+ const OpencodeMcpToggleSchema = z.looseObject({ enabled: z.boolean() }).check(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"));
21822
+ const OpencodeMcpServerSchema = z.union([
21823
+ OpencodeMcpLocalServerSchema,
21824
+ OpencodeMcpRemoteServerSchema,
21825
+ OpencodeMcpToggleSchema
21826
+ ]);
21297
21827
  const OpencodeConfigSchema = z.looseObject({
21298
21828
  $schema: z.optional(z.string()),
21299
21829
  mcp: z.optional(z.record(z.string(), OpencodeMcpServerSchema)),
21300
21830
  tools: z.optional(z.record(z.string(), z.boolean()))
21301
21831
  });
21302
21832
  /**
21833
+ * Tell the two transport arms from a toggle entry. Both carry a `type` literal
21834
+ * and a toggle never does — the schema refuses one that tries — but the toggle
21835
+ * arm is loose, so its index signature hides that from `in` narrowing.
21836
+ */
21837
+ function isOpencodeTransportServer(server) {
21838
+ return server.type === "local" || server.type === "remote";
21839
+ }
21840
+ /**
21303
21841
  * Convert OpenCode native format back to standard MCP format
21304
21842
  * - type: "local" -> "stdio", "remote" -> "sse"
21305
21843
  * - command (array) -> command (first element) + args (rest)
@@ -21324,19 +21862,31 @@ function convertFromOpencodeFormat(opencodeMcp, tools) {
21324
21862
  ...convertOpencodeServers(opencodeMcp, tools)
21325
21863
  };
21326
21864
  }
21865
+ /** Split the shared top-level `tools` map into this server's own two lists. */
21866
+ function splitOpencodeServerTools(serverName, tools) {
21867
+ const enabledTools = [];
21868
+ const disabledTools = [];
21869
+ const prefix = `${serverName}_`;
21870
+ for (const [toolName, enabled] of Object.entries(tools ?? {})) {
21871
+ if (!toolName.startsWith(prefix)) continue;
21872
+ const toolSuffix = toolName.slice(prefix.length);
21873
+ (enabled ? enabledTools : disabledTools).push(toolSuffix);
21874
+ }
21875
+ return {
21876
+ enabledTools,
21877
+ disabledTools
21878
+ };
21879
+ }
21327
21880
  function convertOpencodeServers(opencodeMcp, tools) {
21328
21881
  return Object.fromEntries(Object.entries(opencodeMcp).map(([serverName, serverConfig]) => {
21329
21882
  const extraFields = Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !OPENCODE_KNOWN_SERVER_KEYS.has(key)));
21330
- const enabledTools = [];
21331
- const disabledTools = [];
21332
- const prefix = `${serverName}_`;
21333
- if (tools) {
21334
- for (const [toolName, enabled] of Object.entries(tools)) if (toolName.startsWith(prefix)) {
21335
- const toolSuffix = toolName.slice(prefix.length);
21336
- if (enabled) enabledTools.push(toolSuffix);
21337
- else disabledTools.push(toolSuffix);
21338
- }
21339
- }
21883
+ const { enabledTools, disabledTools } = splitOpencodeServerTools(serverName, tools);
21884
+ if (!isOpencodeTransportServer(serverConfig)) return [serverName, {
21885
+ ...extraFields,
21886
+ disabled: serverConfig.enabled === false,
21887
+ ...enabledTools.length > 0 && { enabledTools },
21888
+ ...disabledTools.length > 0 && { disabledTools }
21889
+ }];
21340
21890
  if (serverConfig.type === "remote") return [serverName, {
21341
21891
  ...extraFields,
21342
21892
  type: "sse",
@@ -21366,7 +21916,20 @@ function convertOpencodeServers(opencodeMcp, tools) {
21366
21916
  }];
21367
21917
  }));
21368
21918
  }
21369
- const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21919
+ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
21920
+ "disabled",
21921
+ "enabledTools",
21922
+ "disabledTools"
21923
+ ]);
21924
+ /**
21925
+ * Warn about the fields a transport-less server loses by being written as a
21926
+ * bare toggle, so the drop is never silent.
21927
+ */
21928
+ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
21929
+ const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
21930
+ if (dropped.length === 0) return;
21931
+ 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.`);
21932
+ }
21370
21933
  /**
21371
21934
  * Convert standard MCP format to OpenCode native format
21372
21935
  * - type: "stdio" -> "local", "sse"/"http" -> "remote"
@@ -21376,11 +21939,27 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21376
21939
  * - enabledTools/disabledTools -> top-level tools map (with server name prefix)
21377
21940
  * - OpenCode-supported extras (timeout, oauth) -> passed through verbatim
21378
21941
  */
21379
- function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21942
+ function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, logger) {
21380
21943
  const serverRecord = serverConfig;
21381
21944
  const passthrough = {};
21382
21945
  for (const key of OPENCODE_PASSTHROUGH_SERVER_FIELDS) if (serverRecord[key] !== void 0) passthrough[key] = serverRecord[key];
21383
21946
  const enabled = serverConfig.disabled !== void 0 ? !serverConfig.disabled : true;
21947
+ if (declaresNoTransport(serverConfig)) {
21948
+ if (serverConfig.disabled === void 0) {
21949
+ if (existingEntry !== void 0 && !isOpencodeTransportServer(existingEntry)) {
21950
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21951
+ return existingEntry;
21952
+ }
21953
+ return warnAndSkipMcpServer({
21954
+ toolName: "OpenCode",
21955
+ serverName,
21956
+ reason: "no transport and no enabled state, so there is nothing to toggle",
21957
+ logger
21958
+ });
21959
+ }
21960
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21961
+ return { enabled };
21962
+ }
21384
21963
  if (isRemoteMcpServer(serverConfig)) {
21385
21964
  const url = resolveRemoteMcpUrl(serverConfig);
21386
21965
  if (url === void 0) return warnAndSkipMcpServer({
@@ -21401,7 +21980,7 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21401
21980
  if (commandArray.length === 0) return warnAndSkipMcpServer({
21402
21981
  toolName: "OpenCode",
21403
21982
  serverName,
21404
- reason: declaresNoTransport(serverConfig) ? "no transport at all" : "a local transport but no command",
21983
+ reason: "a local transport but no command",
21405
21984
  logger
21406
21985
  });
21407
21986
  return {
@@ -21413,11 +21992,11 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21413
21992
  ...serverConfig.cwd && { cwd: serverConfig.cwd }
21414
21993
  };
21415
21994
  }
21416
- function convertToOpencodeFormat(mcpServers, logger) {
21995
+ function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
21417
21996
  const tools = {};
21418
21997
  return {
21419
21998
  mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
21420
- const converted = convertServerToOpencodeFormat(serverName, serverConfig, logger);
21999
+ const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], logger);
21421
22000
  if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[`${serverName}_${tool}`] = true;
21422
22001
  if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[`${serverName}_${tool}`] = false;
21423
22002
  return converted === null ? null : [serverName, converted];
@@ -21487,10 +22066,12 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
21487
22066
  fileContent = await readFileContentOrNull(jsonPath);
21488
22067
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
21489
22068
  }
21490
- const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(convertEnvVarRefsToToolFormat({
22069
+ const transformedServers = convertEnvVarRefsToToolFormat({
21491
22070
  mcpServers: rulesyncMcp.getMcpServers(),
21492
22071
  replacement: "{env:$1}"
21493
- }), logger);
22072
+ });
22073
+ const existingMcp = OpencodeConfigSchema.safeParse(parse(fileContent || "{}"));
22074
+ const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
21494
22075
  return new OpencodeMcp({
21495
22076
  outputRoot,
21496
22077
  relativeDirPath: basePaths.relativeDirPath,
@@ -23800,10 +24381,11 @@ function buildPermissionEntry$1(toolName, pattern) {
23800
24381
  * file (global scope only). The file holds other CLI settings besides
23801
24382
  * permissions, so it is never deleted.
23802
24383
  *
23803
- * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
23804
- * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
23805
- * authored and round-tripped through the `antigravity-cli` permissions override
23806
- * (see `AntigravityCliPermissionsOverrideSchema`).
24384
+ * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
24385
+ * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
24386
+ * `artifactReviewPolicy` and `allowNonWorkspaceAccess` are authored and
24387
+ * round-tripped through the `antigravity-cli` permissions override (see
24388
+ * `AntigravityCliPermissionsOverrideSchema`).
23807
24389
  */
23808
24390
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
23809
24391
  constructor(params) {
@@ -23867,6 +24449,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23867
24449
  const override = config["antigravity-cli"];
23868
24450
  if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
23869
24451
  if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
24452
+ if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
24453
+ if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
23870
24454
  const fileContent = JSON.stringify(merged, null, 2);
23871
24455
  return new AntigravityCliPermissions({
23872
24456
  outputRoot,
@@ -23893,6 +24477,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23893
24477
  const override = {};
23894
24478
  if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
23895
24479
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
24480
+ if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
24481
+ if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
23896
24482
  const result = { ...config };
23897
24483
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
23898
24484
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -24696,12 +25282,57 @@ function parseClaudePermissionEntry(entry) {
24696
25282
  };
24697
25283
  }
24698
25284
  /**
25285
+ * Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
25286
+ * rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
25287
+ * but never matched by those checks, so Claude Code warns at startup for each
25288
+ * allow, deny, or ask rule in one of these unmatched forms" — so a canonical
25289
+ * `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
25290
+ * docs prescribe instead. A tool-name rule with no path is unaffected: it
25291
+ * matches the tool everywhere and produces no warning.
25292
+ * @see https://code.claude.com/docs/en/permissions
25293
+ */
25294
+ function isPlainRecord(value) {
25295
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25296
+ }
25297
+ /**
25298
+ * Merge `patch` into `base`, recursing into plain objects so a sibling key at
25299
+ * any depth survives. Arrays and scalars are replaced, since a list the author
25300
+ * states is the list they mean.
25301
+ */
25302
+ function deepMergeRecords(base, patch) {
25303
+ const merged = { ...base };
25304
+ for (const [key, value] of Object.entries(patch)) {
25305
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
25306
+ const existing = merged[key];
25307
+ merged[key] = isPlainRecord(existing) && isPlainRecord(value) ? deepMergeRecords(existing, value) : value;
25308
+ }
25309
+ return merged;
25310
+ }
25311
+ const CLAUDE_PATH_RULE_ALIASES = {
25312
+ Write: "Edit",
25313
+ NotebookEdit: "Edit",
25314
+ Glob: "Read"
25315
+ };
25316
+ /**
24699
25317
  * Build a Claude Code permission entry like "Bash(npm run *)".
24700
25318
  * If the pattern is "*", returns just the tool name.
24701
25319
  */
24702
25320
  function buildClaudePermissionEntry(toolName, pattern) {
24703
25321
  if (pattern === "*") return toolName;
24704
- return `${toolName}(${pattern})`;
25322
+ return `${CLAUDE_PATH_RULE_ALIASES[toolName] ?? toolName}(${pattern})`;
25323
+ }
25324
+ /**
25325
+ * The Claude tool names the canonical config manages. Deliberately the tool
25326
+ * names the categories map to and *not* the aliases a path rule is rewritten
25327
+ * to: claiming `Edit` because a `write` rule exists would sweep away the
25328
+ * `Read`/`Edit` entries the ignore feature and the user wrote in the same file.
25329
+ * The rewritten entries are still rulesync's to place — `applyPermissions`
25330
+ * replaces an entry this run emits wherever it currently sits — and the
25331
+ * original name stays claimed so an entry an older rulesync wrote in the warned
25332
+ * form is cleaned up on the next generate.
25333
+ */
25334
+ function managedClaudeToolNames(config) {
25335
+ return new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
24705
25336
  }
24706
25337
  var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions {
24707
25338
  constructor(params) {
@@ -24741,7 +25372,10 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24741
25372
  throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
24742
25373
  }
24743
25374
  const config = rulesyncPermissions.getJson();
24744
- const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
25375
+ const { allow, ask, deny } = convertRulesyncToClaudePermissions({
25376
+ config,
25377
+ logger
25378
+ });
24745
25379
  const overridePermissions = config.claudecode?.permissions;
24746
25380
  if (overridePermissions && typeof overridePermissions === "object") {
24747
25381
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
@@ -24750,7 +25384,9 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24750
25384
  ...nonListFields
24751
25385
  };
24752
25386
  }
24753
- const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
25387
+ const overrideSandbox = config.claudecode?.sandbox;
25388
+ if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
25389
+ const managedToolNames = managedClaudeToolNames(config);
24754
25390
  const merged = applyPermissions({
24755
25391
  settings,
24756
25392
  managedToolNames,
@@ -24784,6 +25420,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24784
25420
  });
24785
25421
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
24786
25422
  if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
25423
+ const { sandbox } = settings;
25424
+ if (isPlainRecord(sandbox) && Object.keys(sandbox).length > 0) config.claudecode = {
25425
+ ...config.claudecode,
25426
+ sandbox
25427
+ };
24787
25428
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
24788
25429
  }
24789
25430
  validate() {
@@ -24805,14 +25446,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24805
25446
  /**
24806
25447
  * Convert rulesync permissions config to Claude Code allow/ask/deny arrays.
24807
25448
  */
24808
- function convertRulesyncToClaudePermissions(config) {
25449
+ function convertRulesyncToClaudePermissions({ config, logger }) {
24809
25450
  const allow = [];
24810
25451
  const ask = [];
24811
25452
  const deny = [];
25453
+ const actionByEntry = /* @__PURE__ */ new Map();
24812
25454
  for (const [category, rules] of Object.entries(config.permission)) {
24813
25455
  const claudeToolName = toClaudeToolName(category);
24814
25456
  for (const [pattern, action] of Object.entries(rules)) {
24815
25457
  const entry = buildClaudePermissionEntry(claudeToolName, pattern);
25458
+ const previous = actionByEntry.get(entry);
25459
+ 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.`);
25460
+ actionByEntry.set(entry, action);
24816
25461
  switch (action) {
24817
25462
  case "allow":
24818
25463
  allow.push(entry);
@@ -25625,19 +26270,24 @@ function mapBashActionToDecision(action) {
25625
26270
  //#endregion
25626
26271
  //#region src/features/permissions/copilot-permissions.ts
25627
26272
  /**
25628
- * The flat, dotted VS Code setting key this adapter manages. VS Code stores
25629
- * settings with dotted keys flat at the document top level, so this is a single
25630
- * literal key — not a nested `chat.tools.terminal` path.
26273
+ * The flat, dotted VS Code setting keys this adapter manages, one per canonical
26274
+ * permission category. VS Code stores settings with dotted keys flat at the
26275
+ * document top level, so each is a single literal key — not a nested
26276
+ * `chat.tools.terminal` path. All three share the same
26277
+ * pattern-to-boolean shape, so one conversion covers them.
26278
+ *
26279
+ * The canonical `read` and `write` categories stay unmapped: VS Code has no
26280
+ * read-approval surface, and folding `write` into the edits map alongside
26281
+ * `edit` would make the two indistinguishable on import.
26282
+ *
25631
26283
  * @see https://code.visualstudio.com/docs/agents/approvals
26284
+ * @see https://code.visualstudio.com/docs/copilot/chat/review-code-edits
25632
26285
  */
25633
- const AUTO_APPROVE_KEY = "chat.tools.terminal.autoApprove";
25634
- /**
25635
- * The canonical permission category this adapter maps. Only shell/terminal
25636
- * commands (`bash`) have a clean, high-fidelity representation in VS Code's
25637
- * `chat.tools.terminal.autoApprove` map; other categories (read/edit/webfetch/
25638
- * …) have no terminal-command equivalent and are intentionally not mapped.
25639
- */
25640
- const TERMINAL_CATEGORY = "bash";
26286
+ const AUTO_APPROVE_KEYS = {
26287
+ bash: "chat.tools.terminal.autoApprove",
26288
+ edit: "chat.tools.edits.autoApprove",
26289
+ webfetch: "chat.tools.urls.autoApprove"
26290
+ };
25641
26291
  function asAutoApproveMap(value) {
25642
26292
  if (!isPlainObject$1(value)) return {};
25643
26293
  const result = {};
@@ -25645,6 +26295,21 @@ function asAutoApproveMap(value) {
25645
26295
  return result;
25646
26296
  }
25647
26297
  /**
26298
+ * Render one canonical category's rules as a VS Code auto-approve map. Returns
26299
+ * `undefined` when the category contributes nothing, so the key is retracted
26300
+ * rather than written as an empty object.
26301
+ *
26302
+ * The resulting map replaces the file's existing value wholesale — rulesync
26303
+ * owns these keys, so a rule dropped from the canonical config disappears from
26304
+ * the settings file too.
26305
+ */
26306
+ function buildAutoApproveValue(rules) {
26307
+ const autoApprove = {};
26308
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
26309
+ else if (action === "deny") autoApprove[pattern] = false;
26310
+ return Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
26311
+ }
26312
+ /**
25648
26313
  * Permissions generator for GitHub Copilot Chat in VS Code.
25649
26314
  *
25650
26315
  * VS Code has no standalone, environment-agnostic Copilot policy file (like
@@ -25654,13 +26319,16 @@ function asAutoApproveMap(value) {
25654
26319
  * many unrelated keys, so reads and writes merge into the existing JSON
25655
26320
  * (touching only the one managed key) and the file is never deleted.
25656
26321
  *
25657
- * Scope is deliberately limited to `chat.tools.terminal.autoApprove` the one
25658
- * clean, non-lossy mapping. The canonical `bash` category's per-pattern rules
25659
- * map as: `allow` → `true` (auto-approve), `deny` → `false` (never approve),
25660
- * and `ask` → the entry is OMITTED (VS Code then falls through to its default
25661
- * in-chat approval prompt, i.e. "ask"). Only project scope is modeled: VS Code's
25662
- * user-scope settings.json lives at a platform-dependent path outside rulesync's
25663
- * home-relative global model.
26322
+ * Three canonical categories have a clean, non-lossy representation and are
26323
+ * mapped (see {@link AUTO_APPROVE_KEYS}): `bash`, `edit` and `webfetch`. In
26324
+ * every one, per-pattern rules map as: `allow` → `true` (auto-approve), `deny`
26325
+ * `false` (VS Code then always prompts note this is "never auto-approve",
26326
+ * not a hard block), and `ask` the entry is OMITTED (VS Code falls through to
26327
+ * the same default prompt). A key whose canonical category is absent entirely
26328
+ * is left untouched, so authoring only `bash` rules never disturbs a
26329
+ * hand-written edits or urls map.
26330
+ * Only project scope is modeled: VS Code's user-scope settings.json lives at a
26331
+ * platform-dependent path outside rulesync's home-relative global model.
25664
26332
  */
25665
26333
  var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25666
26334
  constructor(params) {
@@ -25697,11 +26365,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25697
26365
  const paths = CopilotPermissions.getSettablePaths();
25698
26366
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25699
26367
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
25700
- const rules = rulesyncPermissions.getJson().permission[TERMINAL_CATEGORY] ?? {};
25701
- const autoApprove = {};
25702
- for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
25703
- else if (action === "deny") autoApprove[pattern] = false;
25704
- const patchValue = Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
26368
+ const config = rulesyncPermissions.getJson();
26369
+ const patch = {};
26370
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
26371
+ const rules = config.permission[category];
26372
+ if (rules === void 0) continue;
26373
+ patch[settingKey] = buildAutoApproveValue(rules);
26374
+ }
25705
26375
  return new CopilotPermissions({
25706
26376
  outputRoot,
25707
26377
  relativeDirPath: paths.relativeDirPath,
@@ -25710,7 +26380,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25710
26380
  fileKey: sharedConfigFileKey(paths),
25711
26381
  feature: "permissions",
25712
26382
  existingContent,
25713
- patch: { [AUTO_APPROVE_KEY]: patchValue },
26383
+ patch,
25714
26384
  filePath
25715
26385
  }),
25716
26386
  validate: true
@@ -25729,10 +26399,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25729
26399
  } catch (error) {
25730
26400
  throw new Error(`Failed to parse Copilot VS Code settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
25731
26401
  }
25732
- const autoApprove = asAutoApproveMap(settings[AUTO_APPROVE_KEY]);
25733
- const rules = {};
25734
- for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
25735
- const permission = Object.keys(rules).length > 0 ? { [TERMINAL_CATEGORY]: rules } : {};
26402
+ const permission = {};
26403
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
26404
+ const autoApprove = asAutoApproveMap(settings[settingKey]);
26405
+ const rules = {};
26406
+ for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
26407
+ if (Object.keys(rules).length > 0) permission[category] = rules;
26408
+ }
25736
26409
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
25737
26410
  }
25738
26411
  validate() {
@@ -31752,6 +32425,7 @@ const ClaudecodeSkillFrontmatterSchema = z.looseObject({
31752
32425
  arguments: z.optional(z.union([z.string(), z.array(z.string())])),
31753
32426
  context: z.optional(z.string()),
31754
32427
  agent: z.optional(z.string()),
32428
+ background: z.optional(z.boolean()),
31755
32429
  hooks: z.optional(z.looseObject({})),
31756
32430
  shell: z.optional(z.string()),
31757
32431
  "disable-model-invocation": z.optional(z.boolean()),
@@ -31778,6 +32452,7 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
31778
32452
  shell: section.shell
31779
32453
  };
31780
32454
  const definedFields = {
32455
+ background: section.background,
31781
32456
  arguments: section.arguments,
31782
32457
  hooks: section.hooks,
31783
32458
  "disable-model-invocation": resolvedDisableModelInvocation,
@@ -31855,6 +32530,7 @@ var ClaudecodeSkill = class extends ToolSkill {
31855
32530
  ...frontmatter.arguments !== void 0 && { arguments: frontmatter.arguments },
31856
32531
  ...frontmatter.context && { context: frontmatter.context },
31857
32532
  ...frontmatter.agent && { agent: frontmatter.agent },
32533
+ ...frontmatter.background !== void 0 && { background: frontmatter.background },
31858
32534
  ...frontmatter.hooks !== void 0 && { hooks: frontmatter.hooks },
31859
32535
  ...frontmatter.shell && { shell: frontmatter.shell },
31860
32536
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
@@ -32464,7 +33140,9 @@ const CopilotcliSkillFrontmatterSchema = z.looseObject({
32464
33140
  description: z.string(),
32465
33141
  license: z.optional(z.string()),
32466
33142
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
32467
- "argument-hint": z.optional(z.string())
33143
+ "argument-hint": z.optional(z.string()),
33144
+ "user-invocable": z.optional(z.boolean()),
33145
+ "disable-model-invocation": z.optional(z.boolean())
32468
33146
  });
32469
33147
  /**
32470
33148
  * Represents a GitHub Copilot CLI skill directory.
@@ -32523,7 +33201,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32523
33201
  const copilotcliSection = {
32524
33202
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
32525
33203
  ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
32526
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
33204
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
33205
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
33206
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
32527
33207
  };
32528
33208
  const rulesyncFrontmatter = {
32529
33209
  name: frontmatter.name,
@@ -32550,7 +33230,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32550
33230
  description: rulesyncFrontmatter.description,
32551
33231
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
32552
33232
  ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
32553
- ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
33233
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] },
33234
+ ...rulesyncFrontmatter.copilotcli?.["user-invocable"] !== void 0 && { "user-invocable": rulesyncFrontmatter.copilotcli["user-invocable"] },
33235
+ ...rulesyncFrontmatter.copilotcli?.["disable-model-invocation"] !== void 0 && { "disable-model-invocation": rulesyncFrontmatter.copilotcli["disable-model-invocation"] }
32554
33236
  };
32555
33237
  return new CopilotcliSkill({
32556
33238
  outputRoot,
@@ -34220,6 +34902,37 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
34220
34902
  alternativeSkillRoots: [global ? OPENCODE_GLOBAL_SKILL_DIR_PATH : OPENCODE_SKILL_DIR_PATH]
34221
34903
  };
34222
34904
  }
34905
+ /**
34906
+ * Extra skill roots the project configured in `opencode.json` /
34907
+ * `opencode.jsonc` via `skills.paths` ("Additional paths to skill folders").
34908
+ * Without these, skills a project keeps outside `.opencode/skills/` are
34909
+ * invisible to `rulesync import` even though OpenCode loads them.
34910
+ *
34911
+ * Import-only: rulesync keeps writing to its own managed root, so a
34912
+ * configured path is read but never generated into. `skills.urls` is a
34913
+ * remote-fetch surface and is out of scope for a file-based generator.
34914
+ *
34915
+ * Absolute paths and paths escaping the output root are dropped — an import
34916
+ * root is joined onto `outputRoot`, and reaching outside it is not something
34917
+ * a project config should be able to ask for.
34918
+ *
34919
+ * @see https://opencode.ai/config.json
34920
+ */
34921
+ static async getConfiguredImportRoots({ outputRoot, global = false }) {
34922
+ const skills = asOpencodeEntries((await readOpencodeConfig({
34923
+ outputRoot,
34924
+ global
34925
+ })).skills);
34926
+ if (skills === null || !Array.isArray(skills.paths)) return [];
34927
+ const configDir = getOpencodeConfigDir({
34928
+ outputRoot,
34929
+ global
34930
+ });
34931
+ return skills.paths.filter((candidate) => typeof candidate === "string" && candidate !== "" && !isAbsolute(candidate) && !normalize(candidate).startsWith("..")).map((relativeDirPath) => ({
34932
+ outputRoot: configDir,
34933
+ relativeDirPath
34934
+ }));
34935
+ }
34223
34936
  getFrontmatter() {
34224
34937
  return OpenCodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
34225
34938
  }
@@ -36153,12 +36866,19 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36153
36866
  */
36154
36867
  async loadToolDirs() {
36155
36868
  const factory = this.getFactory(this.toolTarget);
36156
- const roots = toolSkillImportRoots(factory.class.getSettablePaths({ global: this.global }));
36869
+ const paths = factory.class.getSettablePaths({ global: this.global });
36870
+ const configuredRoots = factory.class.getConfiguredImportRoots ? await factory.class.getConfiguredImportRoots({
36871
+ outputRoot: this.outputRoot,
36872
+ global: this.global
36873
+ }) : [];
36874
+ const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
36875
+ const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
36157
36876
  const seenSkillNames = /* @__PURE__ */ new Set();
36158
36877
  const toolSkills = [];
36159
36878
  for (const root of roots) {
36160
36879
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
36161
36880
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
36881
+ const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
36162
36882
  const skillsDirPath = join(rootOutputRoot, relativeDirPath);
36163
36883
  if (!await directoryExists(skillsDirPath)) continue;
36164
36884
  const dirPaths = await findFilesByGlobs(join(skillsDirPath, "*"), { type: "dir" });
@@ -36173,12 +36893,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36173
36893
  })) continue;
36174
36894
  ownedDirNames.push(dirName);
36175
36895
  }
36176
- const directorySkills = await Promise.all(ownedDirNames.map((dirName) => factory.class.fromDir({
36177
- outputRoot: rootOutputRoot,
36178
- relativeDirPath,
36179
- dirName,
36180
- global: this.global
36181
- })));
36896
+ const directorySkills = (await Promise.all(ownedDirNames.map(async (dirName) => {
36897
+ try {
36898
+ return await factory.class.fromDir({
36899
+ outputRoot: rootOutputRoot,
36900
+ relativeDirPath,
36901
+ dirName,
36902
+ global: this.global
36903
+ });
36904
+ } catch (error) {
36905
+ if (!isConfiguredRoot) throw error;
36906
+ this.logger.warn(`Skipping ${join(relativeDirPath, dirName)}: ${formatError(error)}`);
36907
+ return null;
36908
+ }
36909
+ }))).filter((skill) => skill !== null);
36182
36910
  for (const skill of directorySkills) {
36183
36911
  const skillName = skill.getImportIdentity();
36184
36912
  if (seenSkillNames.has(skillName)) continue;
@@ -36703,6 +37431,242 @@ var RovodevSubagent = class RovodevSubagent extends ToolSubagent {
36703
37431
  }
36704
37432
  };
36705
37433
  //#endregion
37434
+ //#region src/features/subagents/antigravity-shared-subagent.ts
37435
+ /**
37436
+ * Frontmatter of an Antigravity custom agent (Markdown format, CLI v1.1.6+).
37437
+ *
37438
+ * `name` and `description` are required upstream; the rest are optional and
37439
+ * documented with defaults (`tools: []`, `mainAgent: true`, `subagent: true`,
37440
+ * `model: inherit`, `commandExecutionPolicy: sandbox`, `mcpServers: []`,
37441
+ * `skills`/`plugins`: `[]`). `hidden` and `inheritMcp` appear in the v1.1.6
37442
+ * release notes but not in the documented frontmatter table, so they are
37443
+ * accepted as verbatim passthrough without any behavior modeled around them.
37444
+ * `looseObject` keeps unknown future fields round-tripping.
37445
+ *
37446
+ * @see https://antigravity.google/docs/subagents
37447
+ */
37448
+ const AntigravitySubagentFrontmatterSchema = z.looseObject({
37449
+ name: z.string(),
37450
+ description: z.string().check(z.minLength(1)),
37451
+ tools: z.optional(z.array(z.string())),
37452
+ mainAgent: z.optional(z.boolean()),
37453
+ subagent: z.optional(z.boolean()),
37454
+ model: z.optional(z.string()),
37455
+ commandExecutionPolicy: z.optional(z.string()),
37456
+ mcpServers: z.optional(z.array(z.unknown())),
37457
+ skills: z.optional(z.array(z.string())),
37458
+ plugins: z.optional(z.array(z.string())),
37459
+ hidden: z.optional(z.boolean()),
37460
+ inheritMcp: z.optional(z.boolean())
37461
+ });
37462
+ /**
37463
+ * Shared custom-agent (subagent) implementation for Google Antigravity 2.0,
37464
+ * used by the IDE, the CLI and plugin bundles.
37465
+ *
37466
+ * Antigravity discovers agents at `.agents/agents/<name>.md` (project) and
37467
+ * `~/.gemini/config/agents/<name>.md` (global, shared by the IDE and the CLI
37468
+ * exactly like `~/.gemini/config/hooks.json`). The directory form
37469
+ * (`<name>/agent.md`) is an equivalent alternative upstream; rulesync emits and
37470
+ * imports the flat file form. The body after the frontmatter is the agent's
37471
+ * system prompt.
37472
+ *
37473
+ * Concrete subclasses only supply the rulesync target name they answer to via
37474
+ * {@link AntigravitySharedSubagent.getToolTarget} and, where the shared file is
37475
+ * not involved, the sections they read via
37476
+ * {@link AntigravitySharedSubagent.getReadSectionKeys}.
37477
+ *
37478
+ * @see https://antigravity.google/docs/subagents
37479
+ */
37480
+ var AntigravitySharedSubagent = class extends ToolSubagent {
37481
+ frontmatter;
37482
+ body;
37483
+ constructor({ frontmatter, body, fileContent, ...rest }) {
37484
+ if (rest.validate !== false) {
37485
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
37486
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
37487
+ }
37488
+ super({
37489
+ ...rest,
37490
+ fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
37491
+ });
37492
+ this.frontmatter = frontmatter;
37493
+ this.body = body;
37494
+ }
37495
+ /** The rulesync target name this subagent answers to. */
37496
+ static getToolTarget() {
37497
+ throw new Error("Please implement this method in the subclass.");
37498
+ }
37499
+ /**
37500
+ * Tool-specific sections this target reads, in increasing precedence order.
37501
+ *
37502
+ * `antigravity-ide` and `antigravity-cli` write the very same file, so a
37503
+ * target that read only its own section would silently drop the other's keys
37504
+ * — and which one survived would depend on `--targets` order. Every target
37505
+ * therefore merges the shared `antigravity-ide` → `antigravity-cli` sections
37506
+ * (the CLI block wins, matching the fixed order the MCP feature already uses
37507
+ * for the same shared-output reason), and the plugin target layers its own
37508
+ * section on top of that. Only `getToolTarget()` decides which section an
37509
+ * import writes back into.
37510
+ */
37511
+ static getReadSectionKeys() {
37512
+ return ["antigravity-ide", "antigravity-cli"];
37513
+ }
37514
+ static getSettablePaths({ global = false } = {}) {
37515
+ return { relativeDirPath: global ? ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH : ANTIGRAVITY_AGENTS_DIR_PATH };
37516
+ }
37517
+ getFrontmatter() {
37518
+ return this.frontmatter;
37519
+ }
37520
+ getBody() {
37521
+ return this.body;
37522
+ }
37523
+ toRulesyncSubagent() {
37524
+ const { name, description, ...restFields } = this.frontmatter;
37525
+ return new RulesyncSubagent({
37526
+ outputRoot: ".",
37527
+ frontmatter: {
37528
+ targets: ["*"],
37529
+ name,
37530
+ description,
37531
+ ...Object.keys(restFields).length > 0 && { [this.constructor.getToolTarget()]: restFields }
37532
+ },
37533
+ body: this.body,
37534
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
37535
+ relativeFilePath: this.getRelativeFilePath(),
37536
+ validate: true
37537
+ });
37538
+ }
37539
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
37540
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
37541
+ const mergedSection = Object.assign({}, ...this.getReadSectionKeys().map((key) => rulesyncFrontmatter[key] ?? {}));
37542
+ const toolSection = this.filterToolSpecificSection(mergedSection, ["name", "description"]);
37543
+ const rawFrontmatter = {
37544
+ name: rulesyncFrontmatter.name,
37545
+ description: rulesyncFrontmatter.description || `${rulesyncFrontmatter.name} subagent`,
37546
+ ...toolSection
37547
+ };
37548
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(rawFrontmatter);
37549
+ if (!result.success) throw new Error(`Invalid ${this.getToolTarget()} subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
37550
+ const frontmatter = result.data;
37551
+ const body = rulesyncSubagent.getBody();
37552
+ const paths = this.getSettablePaths({ global });
37553
+ return new this({
37554
+ outputRoot,
37555
+ frontmatter,
37556
+ body,
37557
+ relativeDirPath: paths.relativeDirPath,
37558
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
37559
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
37560
+ validate,
37561
+ global
37562
+ });
37563
+ }
37564
+ validate() {
37565
+ if (!this.frontmatter) return {
37566
+ success: true,
37567
+ error: null
37568
+ };
37569
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(this.frontmatter);
37570
+ if (result.success) return {
37571
+ success: true,
37572
+ error: null
37573
+ };
37574
+ return {
37575
+ success: false,
37576
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
37577
+ };
37578
+ }
37579
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
37580
+ return this.isTargetedByRulesyncSubagentDefault({
37581
+ rulesyncSubagent,
37582
+ toolTarget: this.getToolTarget()
37583
+ });
37584
+ }
37585
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
37586
+ const paths = this.getSettablePaths({ global });
37587
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
37588
+ const fileContent = await readFileContent(filePath);
37589
+ const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
37590
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
37591
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
37592
+ return new this({
37593
+ outputRoot,
37594
+ relativeDirPath: paths.relativeDirPath,
37595
+ relativeFilePath,
37596
+ frontmatter: result.data,
37597
+ body: content.trim(),
37598
+ fileContent,
37599
+ validate,
37600
+ global
37601
+ });
37602
+ }
37603
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
37604
+ return new this({
37605
+ outputRoot,
37606
+ relativeDirPath,
37607
+ relativeFilePath,
37608
+ frontmatter: {
37609
+ name: "",
37610
+ description: ""
37611
+ },
37612
+ body: "",
37613
+ fileContent: "",
37614
+ validate: false,
37615
+ global
37616
+ });
37617
+ }
37618
+ };
37619
+ //#endregion
37620
+ //#region src/features/subagents/antigravity-cli-subagent.ts
37621
+ /**
37622
+ * Google Antigravity CLI custom agent (subagent), shipped in CLI v1.1.6.
37623
+ *
37624
+ * Shares all behavior with {@link AntigravitySharedSubagent}; the CLI reads the
37625
+ * same `.agents/agents/` and `~/.gemini/config/agents/` roots as the IDE and
37626
+ * exposes them through the `agy agents` subcommand and the `--agent` flag. It
37627
+ * answers to the `antigravity-cli` target.
37628
+ */
37629
+ var AntigravityCliSubagent = class extends AntigravitySharedSubagent {
37630
+ static getToolTarget() {
37631
+ return "antigravity-cli";
37632
+ }
37633
+ };
37634
+ //#endregion
37635
+ //#region src/features/subagents/antigravity-ide-subagent.ts
37636
+ /**
37637
+ * Google Antigravity IDE custom agent (subagent).
37638
+ *
37639
+ * Shares all behavior with {@link AntigravitySharedSubagent} — the subagents
37640
+ * documentation is product-wide and lists the same `.agents/agents/` and
37641
+ * `~/.gemini/config/agents/` roots for the IDE and the CLI. It answers to the
37642
+ * `antigravity-ide` target.
37643
+ */
37644
+ var AntigravityIdeSubagent = class extends AntigravitySharedSubagent {
37645
+ static getToolTarget() {
37646
+ return "antigravity-ide";
37647
+ }
37648
+ };
37649
+ //#endregion
37650
+ //#region src/features/subagents/antigravity-plugin-subagent.ts
37651
+ /**
37652
+ * Custom agent inside an Antigravity plugin bundle
37653
+ * (`<plugin_name>/agents/<name>.md`). Plugin bundles are project-scope output
37654
+ * only; they are staged into `~/.gemini/antigravity-cli/plugins/` by the user.
37655
+ *
37656
+ * @see https://antigravity.google/docs/cli/plugins
37657
+ */
37658
+ var AntigravityPluginSubagent = class extends AntigravitySharedSubagent {
37659
+ static getToolTarget() {
37660
+ return "antigravity-plugin";
37661
+ }
37662
+ static getReadSectionKeys() {
37663
+ return [...super.getReadSectionKeys(), "antigravity-plugin"];
37664
+ }
37665
+ static getSettablePaths() {
37666
+ return { relativeDirPath: ANTIGRAVITY_PLUGIN_AGENTS_DIR };
37667
+ }
37668
+ };
37669
+ //#endregion
36706
37670
  //#region src/features/subagents/augmentcode-subagent.ts
36707
37671
  const AugmentcodeSubagentFrontmatterSchema = z.looseObject({
36708
37672
  name: z.string(),
@@ -40280,6 +41244,30 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
40280
41244
  filePattern: "*.md"
40281
41245
  }
40282
41246
  }],
41247
+ ["antigravity-cli", {
41248
+ class: AntigravityCliSubagent,
41249
+ meta: {
41250
+ supportsSimulated: false,
41251
+ supportsGlobal: true,
41252
+ filePattern: "*.md"
41253
+ }
41254
+ }],
41255
+ ["antigravity-ide", {
41256
+ class: AntigravityIdeSubagent,
41257
+ meta: {
41258
+ supportsSimulated: false,
41259
+ supportsGlobal: true,
41260
+ filePattern: "*.md"
41261
+ }
41262
+ }],
41263
+ ["antigravity-plugin", {
41264
+ class: AntigravityPluginSubagent,
41265
+ meta: {
41266
+ supportsSimulated: false,
41267
+ supportsGlobal: false,
41268
+ filePattern: "*.md"
41269
+ }
41270
+ }],
40283
41271
  ["augmentcode", {
40284
41272
  class: AugmentcodeSubagent,
40285
41273
  meta: {
@@ -42493,6 +43481,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
42493
43481
  const CopilotRuleFrontmatterSchema = z.object({
42494
43482
  description: z.optional(z.string()),
42495
43483
  applyTo: z.optional(z.string()),
43484
+ name: z.optional(z.string()),
42496
43485
  excludeAgent: z.optional(z.union([
42497
43486
  z.literal("code-review"),
42498
43487
  z.literal("cloud-agent"),
@@ -42550,7 +43539,10 @@ var CopilotRule = class CopilotRule extends ToolRule {
42550
43539
  root: this.isRoot(),
42551
43540
  description: this.frontmatter.description,
42552
43541
  globs,
42553
- ...this.frontmatter.excludeAgent && { copilot: { excludeAgent: this.frontmatter.excludeAgent } }
43542
+ ...(this.frontmatter.excludeAgent || this.frontmatter.name) && { copilot: {
43543
+ ...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
43544
+ ...this.frontmatter.name && { name: this.frontmatter.name }
43545
+ } }
42554
43546
  };
42555
43547
  const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
42556
43548
  return new RulesyncRule({
@@ -42569,7 +43561,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
42569
43561
  const copilotFrontmatter = {
42570
43562
  description: rulesyncFrontmatter.description,
42571
43563
  applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0,
42572
- excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent
43564
+ excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
43565
+ name: rulesyncFrontmatter.copilot?.name
42573
43566
  };
42574
43567
  const body = rulesyncRule.getBody();
42575
43568
  if (root) return new CopilotRule({
@@ -48038,4 +49031,4 @@ async function importChecksCore(params) {
48038
49031
  //#endregion
48039
49032
  export { ErrorCodes as $, RULESYNC_SKILLS_RELATIVE_DIR_PATH as $t, RulesyncMcp as A, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as At, stringifyFrontmatter as B, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Bt, RulesyncSubagent as C, writeFileContent as Ct, RulesyncRule as D, ToolTargetSchema as Dt, RulesyncSkillFrontmatterSchema as E, PACKAGING_TOOL_TARGETS as Et, parseJsonc as F, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Ft, ConfigFileSchema as G, RULESYNC_MCP_SCHEMA_URL as Gt, SHARED_USER_MANAGED_CONFIG_PATHS as H, RULESYNC_MCP_FILE_NAME as Ht, RulesyncCommand as I, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as It, ConsoleLogger as J, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Jt, SourceEntrySchema as K, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Kt, RulesyncCommandFrontmatterSchema as L, RULESYNC_HOOKS_FILE_NAME as Lt, RulesyncHooks as M, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Mt, getRulesyncSourceCandidates as N, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Nt, RulesyncRuleFrontmatterSchema as O, MAX_FILE_SIZE as Ot, resolveRulesyncSourceWritePath as P, RULESYNC_CONFIG_SCHEMA_URL as Pt, CLIError as Q, RULESYNC_RULES_RELATIVE_DIR_PATH as Qt, RulesyncCheck as R, RULESYNC_HOOKS_LEGACY_FILE_NAME as Rt, getLocalSkillDirNames as S, toPosixPath as St, RulesyncSkill as T, ALL_TOOL_TARGETS_WITH_WILDCARD as Tt, SKILL_FILE_NAME as U, RULESYNC_MCP_LEGACY_FILE_NAME as Ut, loadYaml as V, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Vt, ConfigResolver as W, RULESYNC_MCP_RELATIVE_FILE_PATH as Wt, fallbackLogger as X, RULESYNC_PERMISSIONS_SCHEMA_URL as Xt, JsonLogger as Y, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Yt, warnOnConflictingFlags as Z, RULESYNC_RELATIVE_DIR_PATH as Zt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as _, removeFile as _t, convertFromTool as a, directoryExists as at, CODEXCLI_BASH_RULES_FILE_NAME as b, resolvePath as bt, SubagentsProcessor as c, findFilesByGlobs as ct, IgnoreProcessor as d, isSymlink as dt, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as en, assertDirectoryIfExists as et, HooksProcessor as f, listDirectoryFiles as ft, CLAUDECODE_MEMORIES_DIR_NAME as g, removeDirectoryStrict as gt, CLAUDECODE_LOCAL_RULE_FILE_NAME as h, removeDirectory as ht, getProcessorRegistryEntry as i, formatError as in, createTempDirectory as it, RulesyncIgnore as j, RULESYNC_CHECKS_RELATIVE_DIR_PATH as jt, RulesyncPermissions as k, RULESYNC_AIIGNORE_FILE_NAME as kt, SkillsProcessor as l, getFileSize as lt, CLAUDECODE_DIR as m, readFileContentOrNull as mt, checkRulesyncDirExists as n, ALL_FEATURES as nn, assertWritablePathInsideRoot as nt, isPackagingToolTarget as o, ensureDir as ot, CommandsProcessor as p, readFileContent as pt, findControlCharacter as q, RULESYNC_PERMISSIONS_FILE_NAME as qt, generate as r, ALL_FEATURES_WITH_WILDCARD as rn, checkPathTraversal as rt, RulesProcessor as s, fileExists as st, importFromTool as t, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as tn, assertTreeContainsNoSymlinks as tt, McpProcessor as u, getHomeDirectory as ut, CLAUDECODE_SKILLS_DIR_PATH as v, removeFileStrict as vt, RulesyncSubagentFrontmatterSchema as w, ALL_TOOL_TARGETS as wt, CODEXCLI_DIR as x, runWithDirectoryRollback as xt, ChecksProcessor as y, removeTempDirectory as yt, RulesyncCheckFrontmatterSchema as z, RULESYNC_HOOKS_RELATIVE_FILE_PATH as zt };
48040
49033
 
48041
- //# sourceMappingURL=import-BT5KR_K-.js.map
49034
+ //# sourceMappingURL=import-BmXT7mRS.js.map