rulesync 16.12.0 → 16.14.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.
@@ -412,13 +412,15 @@ const permissionsProcessorToolTargetTuple = [
412
412
  "kiro-cli",
413
413
  "kiro-ide",
414
414
  "opencode",
415
+ "pi",
415
416
  "qwencode",
416
417
  "reasonix",
417
418
  "rovodev",
418
419
  "takt",
419
420
  "vibe",
420
421
  "warp",
421
- "zed"
422
+ "zed",
423
+ "zoocode"
422
424
  ];
423
425
  const checksProcessorToolTargetTuple = [
424
426
  "amp",
@@ -1903,7 +1905,7 @@ function isPlainObject$1(value) {
1903
1905
  /**
1904
1906
  * Type guard to check if a value is an array of strings.
1905
1907
  */
1906
- function isStringArray$1(value) {
1908
+ function isStringArray$2(value) {
1907
1909
  return Array.isArray(value) && value.every((item) => typeof item === "string");
1908
1910
  }
1909
1911
  //#endregion
@@ -2271,6 +2273,7 @@ const HOOK_EVENTS = [
2271
2273
  "todoCreated",
2272
2274
  "todoCompleted",
2273
2275
  "stopFailure",
2276
+ "stopCancelled",
2274
2277
  "instructionsLoaded",
2275
2278
  "userPromptExpansion",
2276
2279
  "postToolBatch",
@@ -2846,6 +2849,7 @@ const GROKCLI_HOOK_EVENTS = [
2846
2849
  "permissionDenied",
2847
2850
  "stop",
2848
2851
  "stopFailure",
2852
+ "stopCancelled",
2849
2853
  "notification",
2850
2854
  "subagentStart",
2851
2855
  "subagentStop",
@@ -2921,16 +2925,10 @@ const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CAN
2921
2925
  /**
2922
2926
  * Hook events supported by Hermes Agent's native Shell Hooks system.
2923
2927
  *
2924
- * Hermes validates hook events against a fixed `VALID_HOOKS` set:
2925
- * `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
2926
- * `pre_verify`, `pre_api_request`, `post_api_request`, `api_request_error`,
2927
- * `on_session_start`, `on_session_end`, `on_session_finalize`,
2928
- * `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
2929
- * `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
2930
- * `transform_terminal_output`, `transform_llm_output`, and the three
2931
- * `kanban_task_*` events. Only the events with a clean 1:1 canonical equivalent
2932
- * are mapped here. All other native events round-trip through
2933
- * `hermesagent.hooks`.
2928
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
2929
+ * of v0.20.2 (`v2026.8.16`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
2930
+ * full list. Only the events with a clean 1:1 canonical equivalent are mapped
2931
+ * here. All other native events round-trip through `hermesagent.hooks`.
2934
2932
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
2935
2933
  */
2936
2934
  const HERMESAGENT_HOOK_EVENTS = [
@@ -2943,6 +2941,21 @@ const HERMESAGENT_HOOK_EVENTS = [
2943
2941
  "subagentStart",
2944
2942
  "subagentStop"
2945
2943
  ];
2944
+ /**
2945
+ * Hermes Agent's native `VALID_HOOKS` set, in upstream source order.
2946
+ *
2947
+ * The authority is `hermes_cli/plugins.py`'s `VALID_HOOKS`, not the docs: at
2948
+ * v0.20.2 `website/docs/user-guide/features/plugins.md` says "26 lifecycle
2949
+ * events" and then lists 30, omitting the five `on_kanban_*` worker/task/dispatch
2950
+ * observers, `gateway_platform_event` and `transform_api_error_classification`.
2951
+ * Shell hooks are gated on the same set.
2952
+ *
2953
+ * Verified against the tag `v2026.8.16` (v0.20.2): `VALID_HOOKS` holds 37
2954
+ * entries, grown from 23 at `v2026.8.3` (v0.20.0). This list holds 36 of them —
2955
+ * see {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
2956
+ *
2957
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py
2958
+ */
2946
2959
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2947
2960
  "pre_tool_call",
2948
2961
  "post_tool_call",
@@ -2951,6 +2964,10 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2951
2964
  "transform_llm_output",
2952
2965
  "pre_llm_call",
2953
2966
  "post_llm_call",
2967
+ "on_stream_start",
2968
+ "on_stream_delta",
2969
+ "on_stream_end",
2970
+ "on_interim_message",
2954
2971
  "pre_verify",
2955
2972
  "pre_api_request",
2956
2973
  "post_api_request",
@@ -2959,16 +2976,39 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2959
2976
  "on_session_end",
2960
2977
  "on_session_finalize",
2961
2978
  "on_session_reset",
2979
+ "on_skill_lifecycle",
2962
2980
  "subagent_start",
2963
2981
  "subagent_stop",
2964
2982
  "pre_gateway_dispatch",
2965
2983
  "pre_approval_request",
2966
2984
  "post_approval_response",
2985
+ "pre_transcription",
2967
2986
  "kanban_task_claimed",
2968
2987
  "kanban_task_completed",
2969
- "kanban_task_blocked"
2988
+ "kanban_task_blocked",
2989
+ "on_kanban_worker_spawned",
2990
+ "on_kanban_worker_exited",
2991
+ "on_kanban_worker_stale_claim",
2992
+ "on_kanban_task_updated",
2993
+ "on_kanban_dispatch_tick",
2994
+ "gateway_platform_event",
2995
+ "pre_command"
2970
2996
  ];
2971
2997
  /**
2998
+ * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
2999
+ * as the shell-hook allow-list, but `SHELL_UNSUPPORTED_HOOKS` is subtracted from
3000
+ * it first: shell hooks cannot return these events' directives, so
3001
+ * `_parse_hooks_block` refuses the registration with a warning rather than
3002
+ * accepting it and dropping the return value.
3003
+ *
3004
+ * rulesync only ever writes shell hooks, so these are excluded from
3005
+ * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
3006
+ *
3007
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
3008
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/agent/shell_hooks.py — `_parse_hooks_block`
3009
+ */
3010
+ const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
3011
+ /**
2972
3012
  * Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
2973
3013
  * snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
2974
3014
  */
@@ -3585,6 +3625,7 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
3585
3625
  permissionDenied: "PermissionDenied",
3586
3626
  stop: "Stop",
3587
3627
  stopFailure: "StopFailure",
3628
+ stopCancelled: "StopCancelled",
3588
3629
  notification: "Notification",
3589
3630
  subagentStart: "SubagentStart",
3590
3631
  subagentStop: "SubagentStop",
@@ -3859,6 +3900,25 @@ const McpServerSchema = z.looseObject({
3859
3900
  kiroAutoApprove: z.optional(z.array(z.string())),
3860
3901
  kiroAutoBlock: z.optional(z.array(z.string())),
3861
3902
  headers: z.optional(z.record(z.string(), z.string())),
3903
+ /**
3904
+ * The canonical per-server tool allowlist.
3905
+ *
3906
+ * **Collision rule for adapters.** Several tools spell an allowlist natively
3907
+ * under a name that also exists here, so a single server entry can carry both
3908
+ * a canonical field and its native counterpart. What the *native* key means
3909
+ * decides the resolution — not which one was written first:
3910
+ *
3911
+ * - Native key is a redundant spelling of the same concept, and both lists are
3912
+ * additive → **merge** them, so a config that already spells the field
3913
+ * natively keeps working (`kiroAutoApprove`/`kiroAutoBlock` in
3914
+ * `kiro-mcp.ts`).
3915
+ * - Native key is the same concept but rulesync owns the generated value, so
3916
+ * keeping both would be ambiguous → the **canonical value wins** and the
3917
+ * native one is dropped with a warning (`tools` in `copilotcli-mcp.ts`).
3918
+ * - Native key of the same name means something else entirely → **refuse the
3919
+ * canonical value** rather than write a shape the tool misreads (`tools` in
3920
+ * `codexcli-mcp.ts`, where Codex reads it as a per-tool approval table).
3921
+ */
3862
3922
  enabledTools: z.optional(z.array(z.string())),
3863
3923
  disabledTools: z.optional(z.array(z.string()))
3864
3924
  });
@@ -4701,8 +4761,65 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4701
4761
  dangerouslyAllowAll: z.optional(z.boolean())
4702
4762
  });
4703
4763
  /**
4764
+ * Tool-scoped override block for Pi Coding Agent. Pi has no allow/ask/deny rule
4765
+ * surface: the only repository-syncable tool gate is `defaultTools`, the list of
4766
+ * built-in tools enabled at startup (added in v0.84.2). It is an enable-list
4767
+ * rather than a canonical allow/deny rule set, so it is authored here rather
4768
+ * than mapped onto `permission`, mirroring `KimiCodePermissionsOverrideSchema`'s
4769
+ * `tools.enabled` and `VibePermissionsOverrideSchema`'s `enabled_tools`.
4770
+ *
4771
+ * An empty array is meaningful upstream — it starts Pi with no built-in tools
4772
+ * while keeping extension and SDK custom tools — so it is emitted as written.
4773
+ * CLI flags (`--tools`, `--no-tools`, `--no-builtin-tools`, `--exclude-tools`)
4774
+ * outrank the setting, and a project array **replaces** the global one rather
4775
+ * than merging with it.
4776
+ *
4777
+ * @example
4778
+ * { "defaultTools": ["bash", "edit", "write"] }
4779
+ * @see https://pi.dev/docs/latest/settings
4780
+ */
4781
+ const PiPermissionsOverrideSchema = z.looseObject({
4782
+ permission: z.optional(ToolScopedPermissionSchema),
4783
+ defaultTools: z.optional(z.array(z.string()))
4784
+ });
4785
+ /**
4786
+ * The Antigravity CLI's four documented `toolPermission` autonomy presets.
4787
+ * Exported so the import side of `antigravity-cli-permissions.ts` filters
4788
+ * against exactly the values the enum below accepts — an unrecognized value
4789
+ * carried in verbatim would fail validation of the *whole*
4790
+ * `.rulesync/permissions.jsonc`, not just that key.
4791
+ * @see https://antigravity.google/docs/cli/reference
4792
+ */
4793
+ const ANTIGRAVITY_CLI_TOOL_PERMISSIONS = [
4794
+ "request-review",
4795
+ "proceed-in-sandbox",
4796
+ "always-proceed",
4797
+ "strict"
4798
+ ];
4799
+ /**
4800
+ * The Antigravity CLI's three documented `artifactReviewPolicy` values. Shared
4801
+ * with the import filter for the same reason as
4802
+ * {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4803
+ * @see https://antigravity.google/docs/cli/settings
4804
+ */
4805
+ const ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES = [
4806
+ "asks-for-review",
4807
+ "agent-decides",
4808
+ "always-proceed"
4809
+ ];
4810
+ /**
4811
+ * The baseline execution modes the CLI persists as `agentMode`. Shared with the
4812
+ * import filter for the same reason as {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4813
+ * @see https://antigravity.google/docs/cli/modes
4814
+ */
4815
+ const ANTIGRAVITY_CLI_AGENT_MODES = [
4816
+ "default",
4817
+ "accept-edits",
4818
+ "plan"
4819
+ ];
4820
+ /**
4704
4821
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4705
- * `settings.json` carries four global autonomy/sandbox knobs outside the
4822
+ * `settings.json` carries five global autonomy/sandbox knobs outside the
4706
4823
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4707
4824
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4708
4825
  * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
@@ -4710,7 +4827,8 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4710
4827
  * agent's artifact changes are gated on a review prompt — `asks-for-review`
4711
4828
  * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4712
4829
  * (a boolean, off by default, letting the agent read or write files outside the
4713
- * active workspace roots). Antigravity applies the allow/deny
4830
+ * active workspace roots), and `agentMode` (the baseline execution mode a session
4831
+ * starts in — `default` / `accept-edits` / `plan`). Antigravity applies the allow/deny
4714
4832
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4715
4833
  * authors these keys verbatim — no precedence modeling is needed on our side.
4716
4834
  * Fields placed here are merged onto the top level of
@@ -4718,27 +4836,27 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4718
4836
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4719
4837
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4720
4838
  * Verified against https://antigravity.google/docs/cli/reference,
4721
- * https://antigravity.google/docs/cli/sandbox and
4722
- * https://antigravity.google/docs/cli/settings.
4839
+ * https://antigravity.google/docs/cli/sandbox,
4840
+ * https://antigravity.google/docs/cli/settings and
4841
+ * https://antigravity.google/docs/cli/modes.
4723
4842
  *
4724
4843
  * @example
4725
4844
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
4726
4845
  */
4727
4846
  const AntigravityCliPermissionsOverrideSchema = z.looseObject({
4728
4847
  permission: z.optional(ToolScopedPermissionSchema),
4729
- toolPermission: z.optional(z.enum([
4730
- "request-review",
4731
- "proceed-in-sandbox",
4732
- "always-proceed",
4733
- "strict"
4734
- ])),
4848
+ toolPermission: z.optional(z.enum(ANTIGRAVITY_CLI_TOOL_PERMISSIONS)),
4735
4849
  enableTerminalSandbox: z.optional(z.boolean()),
4736
- artifactReviewPolicy: z.optional(z.enum([
4737
- "asks-for-review",
4738
- "agent-decides",
4739
- "always-proceed"
4740
- ])),
4741
- allowNonWorkspaceAccess: z.optional(z.boolean())
4850
+ artifactReviewPolicy: z.optional(z.enum(ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES)),
4851
+ allowNonWorkspaceAccess: z.optional(z.boolean()),
4852
+ /**
4853
+ * The baseline execution mode the CLI starts a session in (Agent Mode), the
4854
+ * persisted form of the `/settings` panel's Agent Mode row. Only
4855
+ * `~/.gemini/antigravity-cli/settings.json` is read for it, which is why this
4856
+ * override stays global-only.
4857
+ * @see https://antigravity.google/docs/cli/modes
4858
+ */
4859
+ agentMode: z.optional(z.enum(ANTIGRAVITY_CLI_AGENT_MODES))
4742
4860
  });
4743
4861
  /**
4744
4862
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -5048,6 +5166,7 @@ const PermissionsConfigSchema = z.looseObject({
5048
5166
  takt: z.optional(TaktPermissionsOverrideSchema),
5049
5167
  amp: z.optional(AmpPermissionsOverrideSchema),
5050
5168
  "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema),
5169
+ pi: z.optional(PiPermissionsOverrideSchema),
5051
5170
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
5052
5171
  kiro: z.optional(KiroPermissionsOverrideSchema),
5053
5172
  codexcli: z.optional(CodexcliPermissionsOverrideSchema),
@@ -5227,6 +5346,7 @@ const RulesyncRuleFrontmatterSchema = z.object({
5227
5346
  systemPrompt: z.optional(z.enum(["append"])),
5228
5347
  contextFile: z.optional(z.enum(["override"]))
5229
5348
  })),
5349
+ roo: z.optional(z.looseObject({ mode: z.optional(z.string()) })),
5230
5350
  takt: z.optional(z.looseObject({
5231
5351
  name: z.optional(z.string()),
5232
5352
  extends: z.optional(z.string()),
@@ -5529,6 +5649,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
5529
5649
  cursor: z.optional(z.looseObject({
5530
5650
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
5531
5651
  "disable-model-invocation": z.optional(z.boolean()),
5652
+ "user-invocable": z.optional(z.boolean()),
5532
5653
  metadata: z.optional(z.looseObject({}))
5533
5654
  })),
5534
5655
  factorydroid: z.optional(z.looseObject({
@@ -5753,6 +5874,11 @@ async function getLocalSkillDirNames(outputRoot) {
5753
5874
  * default with a per-target value. A defined section value (including `false`)
5754
5875
  * always wins over the root default.
5755
5876
  *
5877
+ * `devin` also consumes the root-level value — it maps `true` onto a user-only
5878
+ * `triggers` list (see `devin-skill.ts`) — but has no section key of the same
5879
+ * name, so it does not go through this helper. A `devin.triggers` section value
5880
+ * still overrides it.
5881
+ *
5756
5882
  * @returns The resolved boolean, or `undefined` when neither value is set.
5757
5883
  */
5758
5884
  function resolveDisableModelInvocation({ rootFrontmatter, section }) {
@@ -5763,10 +5889,15 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
5763
5889
  *
5764
5890
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
5765
5891
  * that applies to every tool supporting the flag (claudecode, copilot,
5766
- * copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5892
+ * copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5767
5893
  * per-target value. A defined section value (including `false`) always wins
5768
5894
  * over the root default.
5769
5895
  *
5896
+ * `devin` also consumes the root-level value — it maps `false` onto a
5897
+ * model-only `triggers` list (see `devin-skill.ts`) — but has no section key of
5898
+ * the same name, so it does not go through this helper. A `devin.triggers`
5899
+ * section value still overrides it.
5900
+ *
5770
5901
  * @returns The resolved boolean, or `undefined` when neither value is set.
5771
5902
  */
5772
5903
  function resolveUserInvocable({ rootFrontmatter, section }) {
@@ -7616,6 +7747,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7616
7747
  }
7617
7748
  }
7618
7749
  },
7750
+ ".pi/settings.json": {
7751
+ format: "json",
7752
+ invalidRootPolicy: "error",
7753
+ features: { permissions: {
7754
+ kind: "replace-owned-keys",
7755
+ ownedKeys: ["defaultTools"]
7756
+ } }
7757
+ },
7758
+ ".pi/agent/settings.json": {
7759
+ format: "json",
7760
+ invalidRootPolicy: "error",
7761
+ features: { permissions: {
7762
+ kind: "replace-owned-keys",
7763
+ ownedKeys: ["defaultTools"]
7764
+ } }
7765
+ },
7619
7766
  ".github/copilot/settings.json": {
7620
7767
  format: "json",
7621
7768
  invalidRootPolicy: "error",
@@ -7641,7 +7788,9 @@ const SHARED_CONFIG_OWNERSHIP = {
7641
7788
  ownedKeys: [
7642
7789
  "chat.tools.terminal.autoApprove",
7643
7790
  "chat.tools.edits.autoApprove",
7644
- "chat.tools.urls.autoApprove"
7791
+ "chat.tools.urls.autoApprove",
7792
+ "zoo-code.allowedCommands",
7793
+ "zoo-code.deniedCommands"
7645
7794
  ]
7646
7795
  } }
7647
7796
  },
@@ -12202,6 +12351,8 @@ const PI_RULE_FILE_NAME = "AGENTS.md";
12202
12351
  const PI_RULE_OVERRIDE_FILE_NAME = "AGENTS.override.md";
12203
12352
  const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
12204
12353
  const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
12354
+ const PI_SETTINGS_FILE_NAME = "settings.json";
12355
+ const PI_AGENT_DIR_PATH = PI_AGENT_DIR;
12205
12356
  //#endregion
12206
12357
  //#region src/features/commands/pi-command.ts
12207
12358
  /**
@@ -12603,6 +12754,21 @@ const ROO_DIR = ".roo";
12603
12754
  const ROO_COMMANDS_DIR_PATH = join(ROO_DIR, "commands");
12604
12755
  const ROO_SKILLS_DIR_PATH = join(ROO_DIR, "skills");
12605
12756
  const ROO_MCP_FILE_NAME = "mcp.json";
12757
+ /**
12758
+ * Mode slugs Roo/Zoo Code themselves accept for a `rules-{mode}` directory:
12759
+ * the loader builds the directory name by interpolating the active mode slug,
12760
+ * and custom-mode slugs are restricted to this alphabet. Validating against it
12761
+ * also keeps an authored value from escaping `.roo/` through path separators or
12762
+ * `..` segments.
12763
+ */
12764
+ const ROO_MODE_SLUG_PATTERN = /^[a-zA-Z0-9-]+$/;
12765
+ /**
12766
+ * `.roo/rules-{mode}/` — the mode-specific rule directory Roo/Zoo Code load
12767
+ * INSTEAD of `.roo/rules/` while that mode is active. The relative path is the
12768
+ * same in global scope, where it resolves under `~/.roo/`.
12769
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/prompts/sections/custom-instructions.ts
12770
+ */
12771
+ const rooModeRulesDirName = (mode) => `rules-${mode}`;
12606
12772
  const ROO_IGNORE_FILE_NAME = ".rooignore";
12607
12773
  /**
12608
12774
  * Roo Code reads project-level custom modes from a single aggregated
@@ -14126,7 +14292,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
14126
14292
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
14127
14293
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
14128
14294
  const isEmittableString = ({ value }) => isNonEmptyString(value);
14129
- const isEmittableArray = ({ value }) => isStringArray(value);
14295
+ const isEmittableArray = ({ value }) => isStringArray$1(value);
14130
14296
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
14131
14297
  const isImportableArray = ({ value }) => isSafeStringArray(value);
14132
14298
  /**
@@ -14435,7 +14601,7 @@ function isStringRecord(value) {
14435
14601
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
14436
14602
  return Object.values(value).every((v) => typeof v === "string");
14437
14603
  }
14438
- function isStringArray(value) {
14604
+ function isStringArray$1(value) {
14439
14605
  return Array.isArray(value) && value.every((v) => typeof v === "string");
14440
14606
  }
14441
14607
  /** Compare object values without letting key order decide the answer. */
@@ -14467,7 +14633,7 @@ function isSafeStringRecord(value) {
14467
14633
  * skipped wholesale when that read fails.
14468
14634
  */
14469
14635
  function isSafeStringArray(value) {
14470
- return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14636
+ return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14471
14637
  }
14472
14638
  /**
14473
14639
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -14509,7 +14675,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
14509
14675
  return {
14510
14676
  ...url !== void 0 && { url },
14511
14677
  ...isStringRecord(headers) && { headers },
14512
- ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14678
+ ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14513
14679
  };
14514
14680
  }
14515
14681
  if (hookType === "mcp_tool") {
@@ -15492,7 +15658,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
15492
15658
  function parseManifest(fileContent) {
15493
15659
  try {
15494
15660
  const parsed = JSON.parse(fileContent);
15495
- if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
15661
+ if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
15496
15662
  return {
15497
15663
  generatedBy: "rulesync",
15498
15664
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -17127,6 +17293,7 @@ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "po
17127
17293
  const HERMESAGENT_FAIL_CLOSED_EVENT = "pre_tool_call";
17128
17294
  const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
17129
17295
  const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
17296
+ const HERMESAGENT_SHELL_UNSUPPORTED_EVENTS = new Set(HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS);
17130
17297
  /**
17131
17298
  * Whether an entry of the `hooks:` mapping is a hook-event list rather than one
17132
17299
  * of its non-event siblings.
@@ -17215,7 +17382,8 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
17215
17382
  }
17216
17383
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
17217
17384
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
17218
- if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.20.0; preserving it for forward compatibility.`);
17385
+ if (HERMESAGENT_SHELL_UNSUPPORTED_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is Python-plugin-only; Hermes Agent refuses it for shell hooks, so the generated entry will never run.`);
17386
+ else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.20.2; preserving it for forward compatibility.`);
17219
17387
  setHermesHookEntries({
17220
17388
  result,
17221
17389
  event: nativeEvent,
@@ -22339,7 +22507,7 @@ function isCodexStdioServer(config) {
22339
22507
  * @see https://learn.chatgpt.com/docs/extend/mcp
22340
22508
  */
22341
22509
  function isValidRenamedArray(key, value) {
22342
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
22510
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
22343
22511
  }
22344
22512
  /**
22345
22513
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
@@ -22859,6 +23027,49 @@ const resolveCopilotcliServerType = (server) => {
22859
23027
  return isRemoteMcpServer(server) ? "http" : "stdio";
22860
23028
  };
22861
23029
  /**
23030
+ * Copilot CLI's per-server tool allowlist. `tools` selects which of the
23031
+ * server's tools are exposed — `["*"]` (the default) for all, or a list of
23032
+ * names — so the canonical `enabledTools` is written under that name rather
23033
+ * than passed through as a key the CLI ignores.
23034
+ *
23035
+ * When a server carries both, the canonical `enabledTools` wins and the native
23036
+ * `tools` is dropped with a warning. rulesync owns the generated allowlist, and
23037
+ * the `tools` value most likely present is the upstream default `["*"]` — left
23038
+ * to win, it would silently discard a real allowlist in favour of "expose every
23039
+ * tool", which is the failure this mapping exists to prevent. That is the
23040
+ * "canonical wins" branch of the collision rule documented on `enabledTools` in
23041
+ * `src/types/mcp.ts`; `kiro-mcp.ts` merges instead (its native names are
23042
+ * redundant spellings of additive lists) and `codexcli-mcp.ts` refuses instead
23043
+ * (Codex's same-named key means a different thing).
23044
+ *
23045
+ * `disabledTools` has no counterpart upstream (expressing it would need the
23046
+ * server's full tool list), so the target keeps `supportsDisabledTools: false`
23047
+ * and the processor strips it before it reaches here.
23048
+ *
23049
+ * @see https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
23050
+ */
23051
+ function renameEnabledToolsToTools({ server, serverName, logger }) {
23052
+ const { enabledTools, ...rest } = server;
23053
+ if (enabledTools === void 0) return rest;
23054
+ if (rest.tools !== void 0) logger?.warn(`[CopilotcliMcp] MCP server "${serverName}" declares both 'tools' and 'enabledTools'; keeping 'enabledTools' and dropping 'tools'.`);
23055
+ return {
23056
+ ...rest,
23057
+ tools: enabledTools
23058
+ };
23059
+ }
23060
+ /**
23061
+ * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
23062
+ * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
23063
+ * CLI-written entry can be a bare string. Canonical `enabledTools` is
23064
+ * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
23065
+ * through would reject the WHOLE imported MCP file rather than one key. Anything
23066
+ * that is not a string array is therefore left under `tools` untouched, matching
23067
+ * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
23068
+ */
23069
+ function isStringArray(value) {
23070
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
23071
+ }
23072
+ /**
22862
23073
  * Resolves and sets the transport type for each MCP server config.
22863
23074
  * GitHub Copilot CLI requires the "type" field for each server. A server it
22864
23075
  * cannot express — one that names no transport, or names one it carries no way
@@ -22902,7 +23113,11 @@ function addTypeField(mcpServers, logger) {
22902
23113
  }
22903
23114
  const { httpUrl: _httpUrl, ...rest } = parsed;
22904
23115
  result[name] = {
22905
- ...rest,
23116
+ ...renameEnabledToolsToTools({
23117
+ server: rest,
23118
+ serverName: name,
23119
+ logger
23120
+ }),
22906
23121
  type,
22907
23122
  url
22908
23123
  };
@@ -22920,7 +23135,11 @@ function addTypeField(mcpServers, logger) {
22920
23135
  continue;
22921
23136
  }
22922
23137
  result[name] = {
22923
- ...local,
23138
+ ...renameEnabledToolsToTools({
23139
+ server: local,
23140
+ serverName: name,
23141
+ logger
23142
+ }),
22924
23143
  type,
22925
23144
  command: head,
22926
23145
  ...tail.length > 0 && { args: tail }
@@ -22934,11 +23153,19 @@ function addTypeField(mcpServers, logger) {
22934
23153
  function removeTypeField(config) {
22935
23154
  const result = {};
22936
23155
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
22937
- if (server.type !== "stdio") {
22938
- result[name] = server;
23156
+ const { tools, ...withoutTools } = server;
23157
+ const restored = isStringArray(tools) ? {
23158
+ ...withoutTools,
23159
+ enabledTools: tools
23160
+ } : {
23161
+ ...withoutTools,
23162
+ ...tools !== void 0 && { tools }
23163
+ };
23164
+ if (restored.type !== "stdio") {
23165
+ result[name] = restored;
22939
23166
  continue;
22940
23167
  }
22941
- const { type: _, ...rest } = server;
23168
+ const { type: _, ...rest } = restored;
22942
23169
  result[name] = rest;
22943
23170
  }
22944
23171
  return result;
@@ -23590,11 +23817,11 @@ function applyGooseStdioFields(ext, config) {
23590
23817
  if (Array.isArray(command)) {
23591
23818
  if (typeof command[0] === "string") ext.cmd = command[0];
23592
23819
  const rest = command.slice(1).filter((c) => typeof c === "string");
23593
- const args = isStringArray$1(config.args) ? config.args : [];
23820
+ const args = isStringArray$2(config.args) ? config.args : [];
23594
23821
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
23595
23822
  } else if (typeof command === "string") {
23596
23823
  ext.cmd = command;
23597
- if (isStringArray$1(config.args)) ext.args = config.args;
23824
+ if (isStringArray$2(config.args)) ext.args = config.args;
23598
23825
  }
23599
23826
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
23600
23827
  }
@@ -23696,7 +23923,7 @@ function convertFromGooseFormat(extensions) {
23696
23923
  else if (type === "streamable_http") server.type = "http";
23697
23924
  else server.type = "stdio";
23698
23925
  if (typeof ext.cmd === "string") server.command = ext.cmd;
23699
- if (isStringArray$1(ext.args)) server.args = ext.args;
23926
+ if (isStringArray$2(ext.args)) server.args = ext.args;
23700
23927
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
23701
23928
  if (typeof ext.uri === "string") server.url = ext.uri;
23702
23929
  if (isPlainObject$1(ext.headers)) server.headers = omitPrototypePollutionKeys(ext.headers);
@@ -23717,11 +23944,11 @@ function buildGoosePluginStdioServer(config) {
23717
23944
  if (Array.isArray(command)) {
23718
23945
  if (typeof command[0] === "string") server.command = command[0];
23719
23946
  const rest = command.slice(1).filter((c) => typeof c === "string");
23720
- const args = isStringArray$1(config.args) ? config.args : [];
23947
+ const args = isStringArray$2(config.args) ? config.args : [];
23721
23948
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
23722
23949
  } else if (typeof command === "string") {
23723
23950
  server.command = command;
23724
- if (isStringArray$1(config.args)) server.args = config.args;
23951
+ if (isStringArray$2(config.args)) server.args = config.args;
23725
23952
  }
23726
23953
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
23727
23954
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -24053,7 +24280,7 @@ function copyHermesOauth(source) {
24053
24280
  "client_secret"
24054
24281
  ]) if (typeof source[key] === "string") oauth[key] = source[key];
24055
24282
  if (typeof source.redirect_port === "number") oauth.redirect_port = source.redirect_port;
24056
- if (isStringArray$1(source.scopes)) oauth.scopes = source.scopes;
24283
+ if (isStringArray$2(source.scopes)) oauth.scopes = source.scopes;
24057
24284
  return Object.keys(oauth).length > 0 ? oauth : void 0;
24058
24285
  }
24059
24286
  function copyHermesAdvancedFields(source, target) {
@@ -24062,7 +24289,7 @@ function copyHermesAdvancedFields(source, target) {
24062
24289
  target.auth = source.auth;
24063
24290
  copied = true;
24064
24291
  }
24065
- if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
24292
+ if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
24066
24293
  target.client_cert = source.client_cert;
24067
24294
  copied = true;
24068
24295
  }
@@ -24131,8 +24358,8 @@ function copyHermesAdvancedFields(source, target) {
24131
24358
  */
24132
24359
  function buildHermesToolsBlock(config) {
24133
24360
  const tools = {};
24134
- if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
24135
- if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
24361
+ if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
24362
+ if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
24136
24363
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
24137
24364
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
24138
24365
  return tools;
@@ -24144,8 +24371,8 @@ function buildHermesToolsBlock(config) {
24144
24371
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
24145
24372
  */
24146
24373
  function applyHermesToolsBlock(hermesTools, server) {
24147
- if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
24148
- if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24374
+ if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
24375
+ if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24149
24376
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
24150
24377
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
24151
24378
  }
@@ -24171,11 +24398,11 @@ function convertServerToHermes(config) {
24171
24398
  if (Array.isArray(command)) {
24172
24399
  if (typeof command[0] === "string") out.command = command[0];
24173
24400
  const rest = command.slice(1).filter((c) => typeof c === "string");
24174
- const args = isStringArray$1(config.args) ? config.args : [];
24401
+ const args = isStringArray$2(config.args) ? config.args : [];
24175
24402
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
24176
24403
  } else if (typeof command === "string") {
24177
24404
  out.command = command;
24178
- if (isStringArray$1(config.args)) out.args = config.args;
24405
+ if (isStringArray$2(config.args)) out.args = config.args;
24179
24406
  }
24180
24407
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
24181
24408
  } else if (url !== void 0) {
@@ -24225,7 +24452,7 @@ function convertFromHermesFormat(mcpServers) {
24225
24452
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
24226
24453
  const server = {};
24227
24454
  if (typeof config.command === "string") server.command = config.command;
24228
- if (isStringArray$1(config.args)) server.args = config.args;
24455
+ if (isStringArray$2(config.args)) server.args = config.args;
24229
24456
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
24230
24457
  if (typeof config.url === "string") server.url = config.url;
24231
24458
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -25183,13 +25410,17 @@ function mergeToolLists(...lists) {
25183
25410
  *
25184
25411
  * Both native names are documented per-server fields, so a config that already
25185
25412
  * spells them natively keeps working: the two lists are merged rather than
25186
- * one overwriting the other.
25413
+ * one overwriting the other. That is the "merge" branch of the collision rule
25414
+ * documented on `enabledTools` in `src/types/mcp.ts` — it applies here because
25415
+ * the native names are redundant spellings of the same additive lists;
25416
+ * `copilotcli-mcp.ts` lets the canonical value win instead, and
25417
+ * `codexcli-mcp.ts` refuses the canonical value instead.
25187
25418
  * @see https://kiro.dev/docs/mcp/configuration/
25188
25419
  */
25189
25420
  function toKiroMcpServers(servers) {
25190
25421
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25191
25422
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
25192
- const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25423
+ const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25193
25424
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
25194
25425
  return [name, {
25195
25426
  ...rest,
@@ -25213,7 +25444,7 @@ function fromKiroMcpServers(servers) {
25213
25444
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25214
25445
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
25215
25446
  const { autoApprove, ...rest } = server;
25216
- if (!isStringArray$1(autoApprove)) return [name, server];
25447
+ if (!isStringArray$2(autoApprove)) return [name, server];
25217
25448
  return [name, {
25218
25449
  ...rest,
25219
25450
  kiroAutoApprove: autoApprove
@@ -26355,7 +26586,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
26355
26586
  }
26356
26587
  function disabledNamesOf(config) {
26357
26588
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
26358
- return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26589
+ return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26359
26590
  }
26360
26591
  /**
26361
26592
  * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
@@ -26488,7 +26719,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
26488
26719
  return [];
26489
26720
  }
26490
26721
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
26491
- const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26722
+ const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26492
26723
  const managedNameSet = new Set(managedNames);
26493
26724
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
26494
26725
  if (reEnabled.length > 0) logger?.warn(`Rovo Dev MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
@@ -27330,7 +27561,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27330
27561
  meta: {
27331
27562
  supportsProject: true,
27332
27563
  supportsGlobal: true,
27333
- supportsEnabledTools: false,
27564
+ supportsEnabledTools: true,
27334
27565
  supportsDisabledTools: false
27335
27566
  }
27336
27567
  }],
@@ -27563,6 +27794,27 @@ const defaultGetFactory$3 = (target) => {
27563
27794
  if (!factory) throw new Error(`Unsupported tool target: ${target}`);
27564
27795
  return factory;
27565
27796
  };
27797
+ /**
27798
+ * Warn about per-server tool filters the target tool cannot express.
27799
+ *
27800
+ * `enabledTools`/`disabledTools` are canonical fields, so a single
27801
+ * `.rulesync/.mcp.json` can carry a filter that only some targets read. The
27802
+ * unsupported ones are stripped before generation (writing them would produce a
27803
+ * key the tool discards on load), which used to happen silently — the filter
27804
+ * simply did not apply and nothing said so. Warning names the servers whose
27805
+ * filter is being dropped for this target so the gap is visible at generate
27806
+ * time rather than in the tool's behavior.
27807
+ *
27808
+ * Only fields actually present are reported, so a config that never authored
27809
+ * the filter stays quiet.
27810
+ */
27811
+ function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger }) {
27812
+ for (const field of fields) {
27813
+ const serverNames = Object.entries(mcpServers ?? {}).filter(([, serverConfig]) => serverConfig[field] !== void 0).map(([serverName]) => serverName);
27814
+ if (serverNames.length === 0) continue;
27815
+ logger.warn(`${toolTarget} does not read the per-server \`${field}\` MCP tool filter; dropping it from ${serverNames.join(", ")}.`);
27816
+ }
27817
+ }
27566
27818
  var McpProcessor = class extends FeatureProcessor {
27567
27819
  toolTarget;
27568
27820
  global;
@@ -27642,6 +27894,12 @@ var McpProcessor = class extends FeatureProcessor {
27642
27894
  const fieldsToStrip = [];
27643
27895
  if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
27644
27896
  if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
27897
+ warnStrippedMcpServerFields({
27898
+ mcpServers: targetedRulesyncMcp.getJson().mcpServers,
27899
+ fields: fieldsToStrip,
27900
+ toolTarget: this.toolTarget,
27901
+ logger: this.logger
27902
+ });
27645
27903
  const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
27646
27904
  return await factory.class.fromRulesyncMcp({
27647
27905
  outputRoot: this.outputRoot,
@@ -28154,6 +28412,34 @@ function convertAmpToRulesync({ disable, permissions }) {
28154
28412
  //#endregion
28155
28413
  //#region src/features/permissions/antigravity-cli-permissions.ts
28156
28414
  /**
28415
+ * Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
28416
+ * permissions override may author. They are siblings of `permissions`, passed
28417
+ * through verbatim in both directions.
28418
+ */
28419
+ const ANTIGRAVITY_CLI_OVERRIDE_KEYS = [
28420
+ "toolPermission",
28421
+ "enableTerminalSandbox",
28422
+ "artifactReviewPolicy",
28423
+ "allowNonWorkspaceAccess",
28424
+ "agentMode"
28425
+ ];
28426
+ const moduleLogger$2 = fallbackLogger;
28427
+ /**
28428
+ * Narrow an imported `settings.json` value to one of the documented values of
28429
+ * an enum-typed override key. The override schema declares `toolPermission`,
28430
+ * `artifactReviewPolicy` and `agentMode` as `z.enum`, so carrying an
28431
+ * unrecognized value through — a hand-written typo, or a preset a newer
28432
+ * Antigravity release added — would fail validation of the whole imported
28433
+ * `.rulesync/permissions.jsonc` instead of dropping just that key. A dropped
28434
+ * value is warned about rather than discarded silently, so a preset rulesync
28435
+ * does not know about yet is visible instead of quietly missing.
28436
+ */
28437
+ function pickDocumentedValue({ key, value, documented }) {
28438
+ if (typeof value !== "string") return void 0;
28439
+ if (documented.includes(value)) return value;
28440
+ moduleLogger$2.warn(`Antigravity CLI permissions: dropping '${key}: ${value}' on import — it is not one of the documented values (${documented.join(", ")}), and carrying it through would fail validation of the whole permissions file. Author it under the 'antigravity-cli' override by hand once rulesync supports it.`);
28441
+ }
28442
+ /**
28157
28443
  * Deliberate mapping from rulesync canonical permission categories to the
28158
28444
  * Antigravity CLI Fine-Grained Permissions Engine action vocabulary
28159
28445
  * (`read_file`, `write_file`, `read_url`, `command`, `mcp`, plus `execute_url` /
@@ -28228,9 +28514,10 @@ function buildPermissionEntry$1(toolName, pattern) {
28228
28514
  * file (global scope only). The file holds other CLI settings besides
28229
28515
  * permissions, so it is never deleted.
28230
28516
  *
28231
- * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28517
+ * Five CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28232
28518
  * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
28233
- * `artifactReviewPolicy` and `allowNonWorkspaceAccess` — are authored and
28519
+ * `artifactReviewPolicy`, `allowNonWorkspaceAccess` and `agentMode` (the
28520
+ * baseline execution mode) — are authored and
28234
28521
  * round-tripped through the `antigravity-cli` permissions override (see
28235
28522
  * `AntigravityCliPermissionsOverrideSchema`).
28236
28523
  */
@@ -28294,10 +28581,10 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28294
28581
  permissions: mergedPermissions
28295
28582
  };
28296
28583
  const override = config["antigravity-cli"];
28297
- if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
28298
- if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
28299
- if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
28300
- if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
28584
+ for (const key of ANTIGRAVITY_CLI_OVERRIDE_KEYS) {
28585
+ const value = override?.[key];
28586
+ if (value !== void 0) merged[key] = value;
28587
+ }
28301
28588
  const fileContent = JSON.stringify(merged, null, 2);
28302
28589
  return new AntigravityCliPermissions({
28303
28590
  outputRoot,
@@ -28322,10 +28609,26 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28322
28609
  deny: permissions.deny ?? []
28323
28610
  });
28324
28611
  const override = {};
28325
- if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
28612
+ const toolPermission = pickDocumentedValue({
28613
+ key: "toolPermission",
28614
+ value: settings.toolPermission,
28615
+ documented: ANTIGRAVITY_CLI_TOOL_PERMISSIONS
28616
+ });
28617
+ if (toolPermission !== void 0) override.toolPermission = toolPermission;
28326
28618
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
28327
- if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
28619
+ const artifactReviewPolicy = pickDocumentedValue({
28620
+ key: "artifactReviewPolicy",
28621
+ value: settings.artifactReviewPolicy,
28622
+ documented: ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES
28623
+ });
28624
+ if (artifactReviewPolicy !== void 0) override.artifactReviewPolicy = artifactReviewPolicy;
28328
28625
  if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
28626
+ const agentMode = pickDocumentedValue({
28627
+ key: "agentMode",
28628
+ value: settings.agentMode,
28629
+ documented: ANTIGRAVITY_CLI_AGENT_MODES
28630
+ });
28631
+ if (agentMode !== void 0) override.agentMode = agentMode;
28329
28632
  const result = { ...config };
28330
28633
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
28331
28634
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -29151,6 +29454,119 @@ function deepMergeRecords(base, patch) {
29151
29454
  }
29152
29455
  return merged;
29153
29456
  }
29457
+ /**
29458
+ * `sandbox.*` paths Claude Code honors only from user settings, managed settings
29459
+ * and the `--settings` CLI flag. Written into a project `.claude/settings.json`
29460
+ * they are silently ignored, so a committed file would read as though it
29461
+ * enforced a sandbox policy while doing nothing — security-relevant for
29462
+ * `network.strictAllowlist` and the credential-masking keys in particular.
29463
+ * Generation therefore drops them at project scope with a per-key warning and
29464
+ * emits them only under `--global`.
29465
+ *
29466
+ * Deliberately NOT listed:
29467
+ * - `bwrapPath` / `socatPath`: v2.1.232 added them to the managed-settings
29468
+ * approval dialog, which is a consent prompt, not a project-scope rejection.
29469
+ * - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
29470
+ * unit is the individual entry's mode, not the settings key, and the same
29471
+ * lists carry `deny` entries that project settings *do* honor — dropping a
29472
+ * whole list would remove real restrictions. `stripProjectIgnoredMaskEntries`
29473
+ * filters those lists per entry instead.
29474
+ *
29475
+ * @see https://code.claude.com/docs/en/sandboxing
29476
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
29477
+ */
29478
+ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29479
+ ["filesystem", "disabled"],
29480
+ ["network", "strictAllowlist"],
29481
+ ["network", "tlsTerminate"],
29482
+ ["credentials", "allowPlaintextInject"],
29483
+ ["credentials", "awsPairs"],
29484
+ ["credentials", "sigv4"],
29485
+ ["allowAppleEvents"],
29486
+ ["ripgrep"]
29487
+ ];
29488
+ /**
29489
+ * Copy of the authored `sandbox` override with the user/managed-only paths
29490
+ * removed, warning once per dropped path. Only the override copy is filtered —
29491
+ * a value already hand-written in the target file is left untouched, matching
29492
+ * the `qwencode` `security.allowPrivateNetworkHooks` precedent.
29493
+ */
29494
+ function stripGlobalOnlySandboxPaths({ sandbox, relativeFilePath, logger }) {
29495
+ const filtered = structuredClone(sandbox);
29496
+ for (const path of CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS) {
29497
+ const leaf = path.at(-1);
29498
+ if (leaf === void 0) continue;
29499
+ const parentPath = path.slice(0, -1);
29500
+ let parent = filtered;
29501
+ for (const segment of parentPath) {
29502
+ const next = parent[segment];
29503
+ if (!isPlainRecord(next)) {
29504
+ parent = {};
29505
+ break;
29506
+ }
29507
+ parent = next;
29508
+ }
29509
+ if (parent[leaf] === void 0) continue;
29510
+ delete parent[leaf];
29511
+ const [container] = parentPath;
29512
+ if (container !== void 0 && isPlainRecord(filtered[container])) {
29513
+ if (Object.keys(filtered[container]).length === 0) delete filtered[container];
29514
+ }
29515
+ logger?.warn(`Claude Code permissions: 'sandbox.${path.join(".")}' is only honored in user/managed/--settings settings, so it is not written to the project-scoped ${relativeFilePath}. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
29516
+ }
29517
+ return filtered;
29518
+ }
29519
+ /** The `sandbox.credentials` lists whose entries accept `"mode": "mask"`. */
29520
+ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
29521
+ /**
29522
+ * Copy of the authored `sandbox` override with the `credentials.envVars` /
29523
+ * `credentials.files` entries whose `mode` is `"mask"` removed, warning once per
29524
+ * list.
29525
+ *
29526
+ * Masking authorizes the sandbox proxy to send the real credential to the listed
29527
+ * hosts, so Claude Code honors it only from user settings, managed settings and
29528
+ * the `--settings` CLI flag; a `mask` entry in a repository's
29529
+ * `.claude/settings.json` is ignored outright. Keeping it would read as though
29530
+ * the credential were masked while nothing protects it — the one project-scope
29531
+ * gap whose consequence is a live credential leaving the sandbox unmasked.
29532
+ *
29533
+ * Filtered per entry rather than per key, because the same lists carry `deny`
29534
+ * entries that project settings *do* honor. `mode` is matched exactly: an entry
29535
+ * Claude Code cannot read as `mask` is not treated as one either (it degrades
29536
+ * such entries to `deny`), so the "reads as masked but isn't" state this guards
29537
+ * against cannot slip through a differently-spelled value.
29538
+ *
29539
+ * Like `stripGlobalOnlySandboxPaths`, only the override copy is filtered — a
29540
+ * value already in the target file is left untouched, which is why the warning
29541
+ * points at it.
29542
+ *
29543
+ * @see https://code.claude.com/docs/en/sandboxing — "`mask` entries … are all
29544
+ * ignored in a repository's `.claude/settings.json` or
29545
+ * `.claude/settings.local.json`"; the file-entry section states the same
29546
+ * settings-source restriction applies there.
29547
+ */
29548
+ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
29549
+ const credentials = sandbox.credentials;
29550
+ if (!isPlainRecord(credentials)) return sandbox;
29551
+ const filteredCredentials = { ...credentials };
29552
+ let changed = false;
29553
+ for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
29554
+ const list = filteredCredentials[listKey];
29555
+ if (!Array.isArray(list)) continue;
29556
+ const kept = list.filter((entry) => !(isPlainRecord(entry) && entry.mode === "mask"));
29557
+ if (kept.length === list.length) continue;
29558
+ changed = true;
29559
+ const dropped = list.length - kept.length;
29560
+ logger?.warn(`Claude Code permissions: 'sandbox.credentials.${listKey}' entries with 'mode: "mask"' are only honored in user/managed/--settings settings, so ${dropped} of them ${dropped === 1 ? "was" : "were"} not written to the project-scoped ${relativeFilePath}. Author them in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
29561
+ if (kept.length === 0) delete filteredCredentials[listKey];
29562
+ else filteredCredentials[listKey] = kept;
29563
+ }
29564
+ if (!changed) return sandbox;
29565
+ const filtered = { ...sandbox };
29566
+ if (Object.keys(filteredCredentials).length === 0) delete filtered.credentials;
29567
+ else filtered.credentials = filteredCredentials;
29568
+ return filtered;
29569
+ }
29154
29570
  const CLAUDE_PATH_RULE_ALIASES = {
29155
29571
  Write: "Edit",
29156
29572
  NotebookEdit: "Edit",
@@ -29204,7 +29620,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29204
29620
  validate
29205
29621
  });
29206
29622
  }
29207
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
29623
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
29208
29624
  const paths = ClaudecodePermissions.getSettablePaths();
29209
29625
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
29210
29626
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -29228,7 +29644,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29228
29644
  };
29229
29645
  }
29230
29646
  const overrideSandbox = config.claudecode?.sandbox;
29231
- if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
29647
+ if (isPlainRecord(overrideSandbox)) {
29648
+ const scopedSandbox = global ? overrideSandbox : stripProjectIgnoredMaskEntries({
29649
+ sandbox: stripGlobalOnlySandboxPaths({
29650
+ sandbox: overrideSandbox,
29651
+ relativeFilePath: paths.relativeFilePath,
29652
+ logger
29653
+ }),
29654
+ relativeFilePath: paths.relativeFilePath,
29655
+ logger
29656
+ });
29657
+ if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
29658
+ }
29232
29659
  const managedToolNames = managedClaudeToolNames(config);
29233
29660
  const merged = applyPermissions({
29234
29661
  settings,
@@ -31405,7 +31832,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
31405
31832
  function convertGoosePermissionConfigToRulesync(userPermission) {
31406
31833
  const permission = {};
31407
31834
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
31408
- const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
31835
+ const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
31409
31836
  const action = GOOSE_LIST_TO_ACTION[key];
31410
31837
  for (const toolName of toolNames) {
31411
31838
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
@@ -31698,7 +32125,7 @@ const ACTION_RANK = {
31698
32125
  * (mirrors the Cursor adapter's preservation of unmanaged types).
31699
32126
  */
31700
32127
  function unmanagedEntries(existingPermission, key) {
31701
- return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
32128
+ return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
31702
32129
  }
31703
32130
  /**
31704
32131
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -31741,9 +32168,9 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
31741
32168
  * described more than once resolves to the strictest action.
31742
32169
  */
31743
32170
  function parseGrokPermissionArrays(permission) {
31744
- const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
31745
- const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
31746
- const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
32171
+ const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
32172
+ const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
32173
+ const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
31747
32174
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31748
32175
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
31749
32176
  const result = {};
@@ -31925,20 +32352,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
31925
32352
  const permission = clonePermissionBlock(provenance.permission);
31926
32353
  reconcileCommandAllowlist({
31927
32354
  permission,
31928
- commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
32355
+ commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
31929
32356
  });
31930
32357
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
31931
32358
  reconcileNativeDenies({
31932
32359
  permission,
31933
32360
  category: "bash",
31934
- patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
32361
+ patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
31935
32362
  });
31936
32363
  const security = isRecord$1(config.security) ? config.security : {};
31937
32364
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
31938
32365
  reconcileNativeDenies({
31939
32366
  permission,
31940
32367
  category: "webfetch",
31941
- patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
32368
+ patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
31942
32369
  });
31943
32370
  removeEmptyCategories(permission);
31944
32371
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -33126,7 +33553,7 @@ function extractKiroOverride(toolsSettings) {
33126
33553
  if (Object.keys(overrideToolsSettings).length === 0) return void 0;
33127
33554
  return { toolsSettings: overrideToolsSettings };
33128
33555
  }
33129
- function asStringArray(value) {
33556
+ function asStringArray$1(value) {
33130
33557
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
33131
33558
  }
33132
33559
  /**
@@ -33136,8 +33563,8 @@ function asStringArray(value) {
33136
33563
  */
33137
33564
  function rulesFromArrays(settings, allowKey, denyKey) {
33138
33565
  const rules = {};
33139
- for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
33140
- for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
33566
+ for (const pattern of asStringArray$1(settings[allowKey])) rules[pattern] = "allow";
33567
+ for (const pattern of asStringArray$1(settings[denyKey])) rules[pattern] = "deny";
33141
33568
  return rules;
33142
33569
  }
33143
33570
  //#endregion
@@ -33366,6 +33793,115 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
33366
33793
  }
33367
33794
  };
33368
33795
  //#endregion
33796
+ //#region src/features/permissions/pi-permissions.ts
33797
+ /** The one `settings.json` key this feature owns. */
33798
+ const DEFAULT_TOOLS_KEY = "defaultTools";
33799
+ /**
33800
+ * Permissions generator for Pi Coding Agent.
33801
+ *
33802
+ * Pi exposes no allow/ask/deny rule surface, so no canonical permission
33803
+ * category maps onto it. Its one repository-syncable tool gate is `defaultTools` —
33804
+ * the built-in tools enabled at startup (v0.84.2) — authored through the `pi`
33805
+ * permissions override.
33806
+ *
33807
+ * Scope semantics are the **opposite** of the union-merge most targets use:
33808
+ * a project `defaultTools` array *replaces* the global array rather than adding
33809
+ * to it, so the two scopes are written independently and never combined.
33810
+ * Project scope writes `.pi/settings.json`; global scope writes
33811
+ * `~/.pi/agent/settings.json`.
33812
+ *
33813
+ * The file is hand-edited and holds many unrelated keys (`theme`,
33814
+ * `defaultModel`, `packages`, `sessionDir`, ...), so writes go through the
33815
+ * shared-config gateway with `defaultTools` as the only owned key, and the file
33816
+ * is never deleted.
33817
+ *
33818
+ * @see https://pi.dev/docs/latest/settings
33819
+ */
33820
+ var PiPermissions = class PiPermissions extends ToolPermissions {
33821
+ constructor(params) {
33822
+ super({
33823
+ ...params,
33824
+ fileContent: params.fileContent ?? "{}"
33825
+ });
33826
+ }
33827
+ /** `settings.json` holds unrelated user settings, so it must not be deleted. */
33828
+ isDeletable() {
33829
+ return false;
33830
+ }
33831
+ static getSettablePaths({ global = false } = {}) {
33832
+ return {
33833
+ relativeDirPath: global ? PI_AGENT_DIR_PATH : ".pi",
33834
+ relativeFilePath: PI_SETTINGS_FILE_NAME
33835
+ };
33836
+ }
33837
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33838
+ const paths = PiPermissions.getSettablePaths({ global });
33839
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
33840
+ return new PiPermissions({
33841
+ outputRoot,
33842
+ relativeDirPath: paths.relativeDirPath,
33843
+ relativeFilePath: paths.relativeFilePath,
33844
+ fileContent,
33845
+ validate,
33846
+ global
33847
+ });
33848
+ }
33849
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
33850
+ const paths = PiPermissions.getSettablePaths({ global });
33851
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
33852
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
33853
+ const defaultTools = rulesyncPermissions.getJson().pi?.defaultTools;
33854
+ const patch = {};
33855
+ if (defaultTools !== void 0) patch[DEFAULT_TOOLS_KEY] = defaultTools;
33856
+ return new PiPermissions({
33857
+ outputRoot,
33858
+ relativeDirPath: paths.relativeDirPath,
33859
+ relativeFilePath: paths.relativeFilePath,
33860
+ fileContent: applySharedConfigPatch({
33861
+ fileKey: sharedConfigFileKey(paths),
33862
+ feature: "permissions",
33863
+ existingContent,
33864
+ patch,
33865
+ filePath
33866
+ }),
33867
+ validate: true,
33868
+ global
33869
+ });
33870
+ }
33871
+ toRulesyncPermissions() {
33872
+ const defaultTools = this.parseSettings()[DEFAULT_TOOLS_KEY];
33873
+ const config = { permission: {} };
33874
+ if (Array.isArray(defaultTools) && defaultTools.every((tool) => typeof tool === "string")) config.pi = { [DEFAULT_TOOLS_KEY]: defaultTools };
33875
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
33876
+ }
33877
+ parseSettings() {
33878
+ try {
33879
+ return parseSharedConfig({
33880
+ format: "json",
33881
+ fileContent: this.getFileContent()
33882
+ });
33883
+ } catch (error) {
33884
+ throw new Error(`Failed to parse Pi settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
33885
+ }
33886
+ }
33887
+ validate() {
33888
+ return {
33889
+ success: true,
33890
+ error: null
33891
+ };
33892
+ }
33893
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
33894
+ return new PiPermissions({
33895
+ outputRoot,
33896
+ relativeDirPath,
33897
+ relativeFilePath,
33898
+ fileContent: "{}",
33899
+ validate: false,
33900
+ global
33901
+ });
33902
+ }
33903
+ };
33904
+ //#endregion
33369
33905
  //#region src/features/permissions/qwencode-permissions.ts
33370
33906
  /**
33371
33907
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -34429,7 +34965,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
34429
34965
  permission[category] ??= {};
34430
34966
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
34431
34967
  }
34432
- if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34968
+ if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34433
34969
  permission.read ??= {};
34434
34970
  permission.read[path] ??= "allow";
34435
34971
  }
@@ -35406,8 +35942,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
35406
35942
  const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
35407
35943
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
35408
35944
  const config = convertWarpToRulesyncPermissions({
35409
- allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35410
- deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35945
+ allow: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35946
+ deny: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35411
35947
  });
35412
35948
  const warpOverride = {};
35413
35949
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -35940,6 +36476,181 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
35940
36476
  }
35941
36477
  };
35942
36478
  //#endregion
36479
+ //#region src/constants/zoocode-paths.ts
36480
+ /**
36481
+ * Zoo Code is a VS Code extension, so its committable command allow/deny lists
36482
+ * are workspace settings rather than files in the `.roo/` agent-asset tree that
36483
+ * the other Zoo Code features write.
36484
+ *
36485
+ * `zoo-code.allowedCommands` / `zoo-code.deniedCommands` are contributed with no
36486
+ * `scope`, which in VS Code means `window` scope — settable in a workspace's
36487
+ * `.vscode/settings.json` — and `ClineProvider.mergeCommandLists()` unions the
36488
+ * workspace values into the effective auto-approval lists.
36489
+ *
36490
+ * The `zoo-code.*` namespace is Zoo-era (the v3.74.0 rebrand); the archived Roo
36491
+ * Code lineage spelled the same settings `roo-cline.*`, so this surface is
36492
+ * deliberately not shared with the `roo` target.
36493
+ *
36494
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
36495
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/webview/ClineProvider.ts
36496
+ */
36497
+ const ZOOCODE_VSCODE_SETTINGS_DIR = ".vscode";
36498
+ const ZOOCODE_VSCODE_SETTINGS_FILE_NAME = "settings.json";
36499
+ const ZOOCODE_ALLOWED_COMMANDS_KEY = "zoo-code.allowedCommands";
36500
+ const ZOOCODE_DENIED_COMMANDS_KEY = "zoo-code.deniedCommands";
36501
+ //#endregion
36502
+ //#region src/features/permissions/zoocode-permissions.ts
36503
+ /**
36504
+ * The canonical category Zoo Code's command lists correspond to. Zoo Code gates
36505
+ * terminal command execution and nothing else through these settings, so only
36506
+ * `bash` maps; `read`/`write`/`edit`/`webfetch` have no workspace-settable
36507
+ * counterpart in the extension's contributions.
36508
+ */
36509
+ const COMMAND_CATEGORY = "bash";
36510
+ function asStringArray(value) {
36511
+ if (!Array.isArray(value)) return [];
36512
+ return value.filter((entry) => typeof entry === "string");
36513
+ }
36514
+ /**
36515
+ * Split one canonical category's rules into Zoo Code's two command lists.
36516
+ *
36517
+ * Zoo Code matches these entries as command **prefixes**: a command runs
36518
+ * without a confirmation prompt when it starts with an `allowedCommands` entry,
36519
+ * and is refused outright when it starts with a `deniedCommands` entry (deny
36520
+ * wins). `ask` is represented by listing the pattern in neither list, which
36521
+ * leaves Zoo Code's default approval prompt in charge.
36522
+ *
36523
+ * Each list is `undefined` when it would be empty, so the key is retracted from
36524
+ * the settings file rather than written as an empty array — an empty
36525
+ * `allowedCommands` and an absent one mean the same thing to Zoo Code, and the
36526
+ * absent form leaves no rulesync residue behind.
36527
+ */
36528
+ function buildCommandLists(rules) {
36529
+ const allowed = [];
36530
+ const denied = [];
36531
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowed.push(pattern);
36532
+ else if (action === "deny") denied.push(pattern);
36533
+ return {
36534
+ allowed: allowed.length > 0 ? allowed : void 0,
36535
+ denied: denied.length > 0 ? denied : void 0
36536
+ };
36537
+ }
36538
+ /**
36539
+ * Permissions generator for Zoo Code.
36540
+ *
36541
+ * Zoo Code has no policy file in its `.roo/` tree: the committable command
36542
+ * allow/deny lists are VS Code workspace settings
36543
+ * (`zoo-code.allowedCommands` / `zoo-code.deniedCommands` in
36544
+ * `.vscode/settings.json`), which `ClineProvider.mergeCommandLists()` unions
36545
+ * into the lists the auto-approval decision reads. That file is a
36546
+ * general-purpose workspace settings file with many unrelated keys, so reads
36547
+ * and writes merge into the existing JSONC (touching only the two managed keys)
36548
+ * and the file is never deleted.
36549
+ *
36550
+ * Only project scope is modeled: VS Code's user-scope `settings.json` lives at
36551
+ * a platform-dependent path outside rulesync's home-relative global model.
36552
+ *
36553
+ * The `roo` target deliberately does not get this adapter. The settings
36554
+ * namespace is Zoo-era (`roo-cline.*` before the v3.74.0 rebrand), and Roo Code
36555
+ * is EOL with its repository archived, so emitting `zoo-code.*` keys for a
36556
+ * `--targets roo` generate would write settings that Roo itself never reads.
36557
+ *
36558
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
36559
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/auto-approval/commands.ts
36560
+ */
36561
+ var ZoocodePermissions = class ZoocodePermissions extends ToolPermissions {
36562
+ constructor(params) {
36563
+ super({
36564
+ ...params,
36565
+ fileContent: params.fileContent ?? "{}"
36566
+ });
36567
+ }
36568
+ /**
36569
+ * `.vscode/settings.json` is a user-managed workspace file with unrelated
36570
+ * settings, so it must not be deleted.
36571
+ */
36572
+ isDeletable() {
36573
+ return false;
36574
+ }
36575
+ static getSettablePaths(_options = {}) {
36576
+ return {
36577
+ relativeDirPath: ZOOCODE_VSCODE_SETTINGS_DIR,
36578
+ relativeFilePath: ZOOCODE_VSCODE_SETTINGS_FILE_NAME
36579
+ };
36580
+ }
36581
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
36582
+ const paths = ZoocodePermissions.getSettablePaths();
36583
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
36584
+ return new ZoocodePermissions({
36585
+ outputRoot,
36586
+ relativeDirPath: paths.relativeDirPath,
36587
+ relativeFilePath: paths.relativeFilePath,
36588
+ fileContent,
36589
+ validate
36590
+ });
36591
+ }
36592
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions }) {
36593
+ const paths = ZoocodePermissions.getSettablePaths();
36594
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
36595
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
36596
+ const rules = rulesyncPermissions.getJson().permission[COMMAND_CATEGORY];
36597
+ const patch = {};
36598
+ if (rules !== void 0) {
36599
+ const { allowed, denied } = buildCommandLists(rules);
36600
+ patch[ZOOCODE_ALLOWED_COMMANDS_KEY] = allowed;
36601
+ patch[ZOOCODE_DENIED_COMMANDS_KEY] = denied;
36602
+ }
36603
+ return new ZoocodePermissions({
36604
+ outputRoot,
36605
+ relativeDirPath: paths.relativeDirPath,
36606
+ relativeFilePath: paths.relativeFilePath,
36607
+ fileContent: applySharedConfigPatch({
36608
+ fileKey: sharedConfigFileKey(paths),
36609
+ feature: "permissions",
36610
+ existingContent,
36611
+ patch,
36612
+ filePath
36613
+ }),
36614
+ validate: true
36615
+ });
36616
+ }
36617
+ toRulesyncPermissions() {
36618
+ let settings;
36619
+ try {
36620
+ settings = parseSharedConfig({
36621
+ format: "jsonc",
36622
+ fileContent: this.getFileContent() || "{}",
36623
+ filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
36624
+ invalidRootPolicy: "error",
36625
+ jsoncParseErrors: "error"
36626
+ });
36627
+ } catch (error) {
36628
+ throw new Error(`Failed to parse Zoo Code VS Code settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
36629
+ }
36630
+ const rules = {};
36631
+ for (const pattern of asStringArray(settings[ZOOCODE_ALLOWED_COMMANDS_KEY])) rules[pattern] = "allow";
36632
+ for (const pattern of asStringArray(settings[ZOOCODE_DENIED_COMMANDS_KEY])) rules[pattern] = "deny";
36633
+ const permission = {};
36634
+ if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
36635
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
36636
+ }
36637
+ validate() {
36638
+ return {
36639
+ success: true,
36640
+ error: null
36641
+ };
36642
+ }
36643
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
36644
+ return new ZoocodePermissions({
36645
+ outputRoot,
36646
+ relativeDirPath,
36647
+ relativeFilePath,
36648
+ fileContent: "{}",
36649
+ validate: false
36650
+ });
36651
+ }
36652
+ };
36653
+ //#endregion
35943
36654
  //#region src/features/permissions/permissions-processor.ts
35944
36655
  const PermissionsProcessorToolTargetSchema = z.enum(permissionsProcessorToolTargetTuple);
35945
36656
  const toolPermissionsFactories = /* @__PURE__ */ new Map([
@@ -36119,6 +36830,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
36119
36830
  supportsImport: true
36120
36831
  }
36121
36832
  }],
36833
+ ["pi", {
36834
+ class: PiPermissions,
36835
+ meta: {
36836
+ supportsProject: true,
36837
+ supportsGlobal: true,
36838
+ supportsImport: true
36839
+ }
36840
+ }],
36122
36841
  ["qwencode", {
36123
36842
  class: QwencodePermissions,
36124
36843
  meta: {
@@ -36174,6 +36893,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
36174
36893
  supportsGlobal: true,
36175
36894
  supportsImport: true
36176
36895
  }
36896
+ }],
36897
+ ["zoocode", {
36898
+ class: ZoocodePermissions,
36899
+ meta: {
36900
+ supportsProject: true,
36901
+ supportsGlobal: false,
36902
+ supportsImport: true
36903
+ }
36177
36904
  }]
36178
36905
  ]);
36179
36906
  var PermissionsProcessor = class extends FeatureProcessor {
@@ -38430,6 +39157,7 @@ const CursorSkillFrontmatterSchema = z.looseObject({
38430
39157
  description: z.string(),
38431
39158
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
38432
39159
  "disable-model-invocation": z.optional(z.boolean()),
39160
+ "user-invocable": z.optional(z.boolean()),
38433
39161
  metadata: z.optional(z.looseObject({}))
38434
39162
  });
38435
39163
  /**
@@ -38484,6 +39212,7 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38484
39212
  const cursorSection = {
38485
39213
  ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
38486
39214
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
39215
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
38487
39216
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
38488
39217
  };
38489
39218
  const rulesyncFrontmatter = {
@@ -38511,11 +39240,16 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38511
39240
  rootFrontmatter: rulesyncFrontmatter,
38512
39241
  section: cursorSection
38513
39242
  });
39243
+ const resolvedUserInvocable = resolveUserInvocable({
39244
+ rootFrontmatter: rulesyncFrontmatter,
39245
+ section: cursorSection
39246
+ });
38514
39247
  const cursorFrontmatter = {
38515
39248
  name: rulesyncFrontmatter.name,
38516
39249
  description: rulesyncFrontmatter.description,
38517
39250
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
38518
39251
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
39252
+ ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
38519
39253
  ...cursorSection?.metadata !== void 0 && { metadata: cursorSection.metadata }
38520
39254
  };
38521
39255
  return new CursorSkill({
@@ -46518,6 +47252,23 @@ var TaktSubagent = class TaktSubagent extends ToolSubagent {
46518
47252
  };
46519
47253
  //#endregion
46520
47254
  //#region src/features/subagents/vibe-subagent.ts
47255
+ /**
47256
+ * Where Vibe reads a custom system prompt from: `.vibe/prompts/<id>.md`
47257
+ * (project) and `~/.vibe/prompts/<id>.md` (user), with the custom directories
47258
+ * winning over the builtins. The id must be a bare file name with no path
47259
+ * separators.
47260
+ * @see vibe/core/prompts/__init__.py
47261
+ */
47262
+ const VIBE_PROMPTS_DIR_PATH = join(".vibe", "prompts");
47263
+ /**
47264
+ * Vibe resolves a `system_prompt_id` by file name, and
47265
+ * `VibeConfigSchema._check_system_prompt` evaluates it during validation — an
47266
+ * unresolvable id raises and the agent is dropped at discovery. The id must
47267
+ * therefore stay a bare slug with no path separators.
47268
+ */
47269
+ function promptSlug(relativeFilePath) {
47270
+ return basename(relativeFilePath).replace(/\.(md|toml)$/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
47271
+ }
46521
47272
  const VibeSubagentTomlSchema = z.looseObject({
46522
47273
  agent_type: z.enum(["agent", "subagent"]),
46523
47274
  display_name: z.optional(z.string()),
@@ -46534,14 +47285,22 @@ const VibeSubagentTomlSchema = z.looseObject({
46534
47285
  });
46535
47286
  var VibeSubagent = class VibeSubagent extends ToolSubagent {
46536
47287
  body;
46537
- constructor({ body, ...rest }) {
46538
- if (rest.validate !== false) try {
47288
+ promptFile;
47289
+ resolvedSystemPrompt;
47290
+ constructor({ body, promptFile = false, resolvedSystemPrompt, ...rest }) {
47291
+ if (rest.validate !== false && !promptFile) try {
46539
47292
  VibeSubagentTomlSchema.parse(smolToml.parse(body));
46540
47293
  } catch (error) {
46541
47294
  throw new Error(`Invalid TOML in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(error)}`, { cause: error });
46542
47295
  }
46543
47296
  super({ ...rest });
46544
47297
  this.body = body;
47298
+ this.promptFile = promptFile;
47299
+ this.resolvedSystemPrompt = resolvedSystemPrompt;
47300
+ }
47301
+ /** True for the companion `.vibe/prompts/<id>.md` file. */
47302
+ isPromptFile() {
47303
+ return this.promptFile;
46545
47304
  }
46546
47305
  static getSettablePaths(_options = {}) {
46547
47306
  return { relativeDirPath: join(".vibe", "agents") };
@@ -46556,7 +47315,9 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46556
47315
  } catch (error) {
46557
47316
  throw new Error(`Failed to parse TOML in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
46558
47317
  }
46559
- const { system_prompt, description, display_name, ...vibeSection } = parsed;
47318
+ const { system_prompt, system_prompt_id, description, display_name, ...vibeSection } = parsed;
47319
+ const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
47320
+ const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
46560
47321
  const fileStem = basename(this.getRelativeFilePath(), ".toml");
46561
47322
  const rulesyncFrontmatter = {
46562
47323
  targets: ["vibe"],
@@ -46565,13 +47326,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46565
47326
  vibe: {
46566
47327
  ...display_name !== void 0 && { display_name },
46567
47328
  ...description !== void 0 && { description },
47329
+ ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
46568
47330
  ...vibeSection
46569
47331
  }
46570
47332
  };
46571
47333
  return new RulesyncSubagent({
46572
47334
  outputRoot: this.outputRoot,
46573
47335
  frontmatter: rulesyncFrontmatter,
46574
- body: system_prompt ?? "",
47336
+ body: resolvedBody ?? "",
46575
47337
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
46576
47338
  relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
46577
47339
  validate: true
@@ -46586,12 +47348,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46586
47348
  "description",
46587
47349
  "system_prompt"
46588
47350
  ]);
47351
+ const promptBody = rulesyncSubagent.getBody();
47352
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
46589
47353
  const tomlObj = {
46590
47354
  agent_type: rawSection.agent_type === "agent" ? "agent" : "subagent",
46591
47355
  display_name: typeof rawSection.display_name === "string" ? rawSection.display_name : frontmatter.name,
46592
47356
  ...frontmatter.description ? { description: frontmatter.description } : {},
46593
- ...rulesyncSubagent.getBody() ? { system_prompt: rulesyncSubagent.getBody() } : {},
46594
- ...vibeSection
47357
+ ...vibeSection,
47358
+ ...promptBody ? { system_prompt_id: slug } : {}
46595
47359
  };
46596
47360
  const body = smolToml.stringify(tomlObj);
46597
47361
  const paths = this.getSettablePaths({ global });
@@ -46606,6 +47370,36 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46606
47370
  global
46607
47371
  });
46608
47372
  }
47373
+ /**
47374
+ * Emits the agent TOML plus its companion prompt file. The prompt file MUST
47375
+ * be written together with the id: `VibeConfigSchema._check_system_prompt`
47376
+ * evaluates the property during validation, so an unresolvable
47377
+ * `system_prompt_id` raises and `AgentRegistry._try_load` drops the agent
47378
+ * with a warning.
47379
+ */
47380
+ static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false }) {
47381
+ return rulesyncSubagents.flatMap((rulesyncSubagent) => {
47382
+ const agentFile = VibeSubagent.fromRulesyncSubagent({
47383
+ outputRoot,
47384
+ relativeDirPath: VibeSubagent.getSettablePaths({ global }).relativeDirPath,
47385
+ rulesyncSubagent,
47386
+ global
47387
+ });
47388
+ const promptBody = rulesyncSubagent.getBody();
47389
+ if (!promptBody) return [agentFile];
47390
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
47391
+ return [agentFile, new VibeSubagent({
47392
+ outputRoot,
47393
+ body: promptBody,
47394
+ relativeDirPath: VIBE_PROMPTS_DIR_PATH,
47395
+ relativeFilePath: `${slug}.md`,
47396
+ fileContent: promptBody,
47397
+ promptFile: true,
47398
+ validate: false,
47399
+ global
47400
+ })];
47401
+ });
47402
+ }
46609
47403
  validate() {
46610
47404
  try {
46611
47405
  VibeSubagentTomlSchema.parse(smolToml.parse(this.body));
@@ -46630,6 +47424,10 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46630
47424
  const paths = this.getSettablePaths({ global });
46631
47425
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
46632
47426
  const fileContent = await readFileContent(filePath);
47427
+ const resolvedSystemPrompt = await VibeSubagent.readSystemPrompt({
47428
+ outputRoot,
47429
+ fileContent
47430
+ });
46633
47431
  const subagent = new VibeSubagent({
46634
47432
  outputRoot,
46635
47433
  relativeDirPath: paths.relativeDirPath,
@@ -46637,7 +47435,8 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46637
47435
  body: fileContent.trim(),
46638
47436
  fileContent,
46639
47437
  validate,
46640
- global
47438
+ global,
47439
+ ...resolvedSystemPrompt !== void 0 && { resolvedSystemPrompt }
46641
47440
  });
46642
47441
  if (validate) {
46643
47442
  const result = subagent.validate();
@@ -46645,6 +47444,21 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46645
47444
  }
46646
47445
  return subagent;
46647
47446
  }
47447
+ /**
47448
+ * Read `.vibe/prompts/<id>.md` for an agent that names a `system_prompt_id`.
47449
+ * Returns `undefined` when there is no id or the file is missing, in which
47450
+ * case import falls back to the legacy `system_prompt` key.
47451
+ */
47452
+ static async readSystemPrompt({ outputRoot, fileContent }) {
47453
+ let promptId;
47454
+ try {
47455
+ promptId = VibeSubagentTomlSchema.parse(smolToml.parse(fileContent)).system_prompt_id;
47456
+ } catch {
47457
+ return;
47458
+ }
47459
+ if (typeof promptId !== "string" || promptId === "" || /[\\/]/.test(promptId)) return;
47460
+ return await readFileContentOrNull(join(outputRoot, VIBE_PROMPTS_DIR_PATH, `${promptId}.md`)) ?? void 0;
47461
+ }
46648
47462
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46649
47463
  return new VibeSubagent({
46650
47464
  outputRoot,
@@ -46687,7 +47501,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46687
47501
  const mode = super.toRooMode(rulesyncSubagent);
46688
47502
  const frontmatter = rulesyncSubagent.getFrontmatter();
46689
47503
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
46690
- if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
47504
+ if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
46691
47505
  return mode;
46692
47506
  }
46693
47507
  toRulesyncSubagents() {
@@ -46699,7 +47513,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46699
47513
  targets: ["zoocode"],
46700
47514
  roo: restRooSection
46701
47515
  };
46702
- if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
47516
+ if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
46703
47517
  return new RulesyncSubagent({
46704
47518
  outputRoot: ".",
46705
47519
  frontmatter: rebuilt,
@@ -49602,8 +50416,13 @@ const STRATEGIES = [
49602
50416
  * file per rule under `.devin/rules/*.md` (Devin Desktop Cascade) with YAML
49603
50417
  * frontmatter carrying a `trigger` (always_on | glob | manual | model_decision)
49604
50418
  * plus companion `globs`/`description` fields.
49605
- * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
49606
- * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
50419
+ * - Global scope: mirrors the project layout. The root rule is a single
50420
+ * plain-markdown, always-on file (no frontmatter) at
50421
+ * `~/.config/devin/AGENTS.md`; non-root rules are one file per rule under
50422
+ * `~/.devin/rules/*.md` with the same `trigger`/`globs` frontmatter as project
50423
+ * scope. Note the directory split — the per-rule global directory is the home
50424
+ * `~/.devin/`, not the `~/.config/devin/` tree every other Devin global
50425
+ * surface uses. That is what the rules page documents.
49607
50426
  *
49608
50427
  * Trigger inference (when no explicit devin trigger is persisted):
49609
50428
  * - Specific globs (non wildcard) → glob
@@ -49634,19 +50453,32 @@ var DevinRule = class DevinRule extends ToolRule {
49634
50453
  relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
49635
50454
  };
49636
50455
  }
50456
+ /**
50457
+ * The always-on root rule file for a scope: the project-root `AGENTS.md`
50458
+ * Devin CLI / Devin Local reads, or `~/.config/devin/AGENTS.md` in global
50459
+ * scope. Single-sourced so `getSettablePaths`, `fromFile` and
50460
+ * `fromRulesyncRule` cannot disagree about it.
50461
+ */
50462
+ static getRootPath(global, excludeToolDir) {
50463
+ return global ? DevinRule.getGlobalRootPath(excludeToolDir) : {
50464
+ relativeDirPath: ".",
50465
+ relativeFilePath: "AGENTS.md"
50466
+ };
50467
+ }
49637
50468
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
49638
- if (global) return { root: DevinRule.getGlobalRootPath(excludeToolDir) };
50469
+ if (global) return {
50470
+ root: DevinRule.getRootPath(true, excludeToolDir),
50471
+ nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
50472
+ };
49639
50473
  return {
49640
- root: {
49641
- relativeDirPath: ".",
49642
- relativeFilePath: "AGENTS.md"
49643
- },
50474
+ root: DevinRule.getRootPath(false, excludeToolDir),
49644
50475
  nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
49645
50476
  };
49646
50477
  }
49647
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
49648
- if (global) {
49649
- const rootPath = DevinRule.getGlobalRootPath();
50478
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
50479
+ const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
50480
+ if (relativeDirPath !== void 0 ? relativeDirPath !== nonRootDirPath : relativeFilePath === DevinRule.getRootPath(global).relativeFilePath) {
50481
+ const rootPath = DevinRule.getRootPath(global);
49650
50482
  const fileContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
49651
50483
  return new DevinRule({
49652
50484
  outputRoot,
@@ -49658,19 +50490,6 @@ var DevinRule = class DevinRule extends ToolRule {
49658
50490
  root: true
49659
50491
  });
49660
50492
  }
49661
- if (relativeFilePath === "AGENTS.md") {
49662
- const fileContent = await readFileContent(join(outputRoot, relativeFilePath));
49663
- return new DevinRule({
49664
- outputRoot,
49665
- relativeDirPath: ".",
49666
- relativeFilePath: "AGENTS.md",
49667
- frontmatter: {},
49668
- body: fileContent,
49669
- validate,
49670
- root: true
49671
- });
49672
- }
49673
- const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
49674
50493
  const filePath = join(outputRoot, nonRootDirPath, relativeFilePath);
49675
50494
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
49676
50495
  let parsedFrontmatter;
@@ -49690,8 +50509,9 @@ var DevinRule = class DevinRule extends ToolRule {
49690
50509
  });
49691
50510
  }
49692
50511
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
49693
- if (global) {
49694
- const rootPath = DevinRule.getGlobalRootPath();
50512
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
50513
+ if (rulesyncFrontmatter.root) {
50514
+ const rootPath = DevinRule.getRootPath(global);
49695
50515
  return new DevinRule({
49696
50516
  outputRoot,
49697
50517
  relativeDirPath: rootPath.relativeDirPath,
@@ -49702,16 +50522,6 @@ var DevinRule = class DevinRule extends ToolRule {
49702
50522
  root: true
49703
50523
  });
49704
50524
  }
49705
- const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
49706
- if (rulesyncFrontmatter.root) return new DevinRule({
49707
- outputRoot,
49708
- relativeDirPath: ".",
49709
- relativeFilePath: "AGENTS.md",
49710
- frontmatter: {},
49711
- body: rulesyncRule.getBody(),
49712
- validate,
49713
- root: true
49714
- });
49715
50525
  const storedDevin = rulesyncFrontmatter.devin;
49716
50526
  const normalized = normalizeStoredDevin(storedDevin);
49717
50527
  const storedTrigger = storedDevin?.trigger;
@@ -49771,7 +50581,7 @@ var DevinRule = class DevinRule extends ToolRule {
49771
50581
  error: null
49772
50582
  };
49773
50583
  }
49774
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50584
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
49775
50585
  return new DevinRule({
49776
50586
  outputRoot,
49777
50587
  relativeDirPath,
@@ -49779,7 +50589,7 @@ var DevinRule = class DevinRule extends ToolRule {
49779
50589
  frontmatter: {},
49780
50590
  body: "",
49781
50591
  validate: false,
49782
- root: global
50592
+ root: relativeDirPath !== buildToolPath(DEVIN_DIR, "rules")
49783
50593
  });
49784
50594
  }
49785
50595
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -50412,6 +51222,11 @@ var KimiCodeRule = class KimiCodeRule extends ToolRule {
50412
51222
  };
50413
51223
  //#endregion
50414
51224
  //#region src/features/rules/kiro-rule.ts
51225
+ /**
51226
+ * Kiro loads `AGENTS.md` as steering context from any directory in the
51227
+ * workspace tree (CLI 2.18.0 / IDE 1.0.309), not just the workspace root.
51228
+ */
51229
+ const KIRO_NESTED_STEERING_FILE_NAME = "AGENTS.md";
50415
51230
  const WILDCARD_GLOBS = /* @__PURE__ */ new Set([
50416
51231
  "**/*",
50417
51232
  "**",
@@ -50485,8 +51300,17 @@ var KiroRule = class KiroRule extends ToolRule {
50485
51300
  };
50486
51301
  return { nonRoot: { relativeDirPath: steeringDirPath } };
50487
51302
  }
50488
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
51303
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
50489
51304
  const paths = this.getSettablePaths({ global });
51305
+ const steeringDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
51306
+ if (overrideDirPath !== void 0 && overrideDirPath !== steeringDirPath && overrideDirPath !== "." && relativeFilePath === KIRO_NESTED_STEERING_FILE_NAME) return new KiroRule({
51307
+ outputRoot,
51308
+ relativeDirPath: overrideDirPath,
51309
+ relativeFilePath,
51310
+ fileContent: await readFileContent(join(outputRoot, overrideDirPath, relativeFilePath)),
51311
+ validate,
51312
+ root: false
51313
+ });
50490
51314
  const isRoot = "root" in paths && relativeFilePath === paths.root.relativeFilePath;
50491
51315
  const relativeDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
50492
51316
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
@@ -50510,6 +51334,12 @@ var KiroRule = class KiroRule extends ToolRule {
50510
51334
  });
50511
51335
  if (params.root) return new KiroRule(params);
50512
51336
  const frontmatter = rulesyncRule.getFrontmatter();
51337
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
51338
+ if (!global && subprojectPath) return new KiroRule({
51339
+ ...params,
51340
+ relativeDirPath: join(subprojectPath),
51341
+ relativeFilePath: KIRO_NESTED_STEERING_FILE_NAME
51342
+ });
50513
51343
  const inclusion = deriveKiroInclusion({
50514
51344
  kiro: frontmatter.kiro,
50515
51345
  globs: frontmatter.globs
@@ -50520,7 +51350,34 @@ var KiroRule = class KiroRule extends ToolRule {
50520
51350
  fileContent: stringifyFrontmatter(rulesyncRule.getBody(), inclusion)
50521
51351
  });
50522
51352
  }
51353
+ /**
51354
+ * The subproject directory a nested `AGENTS.md` scopes, or `undefined` for the
51355
+ * root rule and for the `.kiro/steering/` files.
51356
+ */
51357
+ getSubprojectPath() {
51358
+ if (this.isRoot() || this.getRelativeFilePath() !== KIRO_NESTED_STEERING_FILE_NAME) return;
51359
+ const relativeDirPath = toPosixPath(this.getRelativeDirPath());
51360
+ if (relativeDirPath === "." || relativeDirPath === "" || relativeDirPath.startsWith(".")) return;
51361
+ return relativeDirPath;
51362
+ }
50523
51363
  toRulesyncRule() {
51364
+ const subprojectPath = this.getSubprojectPath();
51365
+ if (subprojectPath !== void 0) {
51366
+ const slug = subprojectPath.replaceAll("/", "-");
51367
+ return new RulesyncRule({
51368
+ outputRoot: this.getOutputRoot(),
51369
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
51370
+ relativeFilePath: `${slug}-kiro.md`,
51371
+ frontmatter: {
51372
+ targets: ["kiro"],
51373
+ root: false,
51374
+ globs: [`${subprojectPath}/**/*`],
51375
+ agentsmd: { subprojectPath }
51376
+ },
51377
+ body: this.getFileContent(),
51378
+ validate: true
51379
+ });
51380
+ }
50524
51381
  const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getRelativeFilePath());
50525
51382
  const inclusion = typeof frontmatter.inclusion === "string" ? frontmatter.inclusion : void 0;
50526
51383
  if (!inclusion) return this.toRulesyncRuleDefault();
@@ -50553,6 +51410,26 @@ var KiroRule = class KiroRule extends ToolRule {
50553
51410
  error: null
50554
51411
  };
50555
51412
  }
51413
+ /**
51414
+ * Nested `AGENTS.md` files anywhere in the workspace tree, which Kiro loads as
51415
+ * steering context alongside `.kiro/steering/`. Import-only, like every other
51416
+ * nested scan: these are hand-authored files outside a rulesync-owned
51417
+ * directory, so enumerating them for `--delete` would sweep away work rulesync
51418
+ * never wrote.
51419
+ * @see https://kiro.dev/docs/steering/
51420
+ */
51421
+ static getNestedFilePatterns({ outputRoot }) {
51422
+ const root = toPosixPath(outputRoot);
51423
+ return {
51424
+ include: [`${root}/**/${KIRO_NESTED_STEERING_FILE_NAME}`],
51425
+ ignore: [
51426
+ `${root}/${KIRO_NESTED_STEERING_FILE_NAME}`,
51427
+ `${root}/**/.*/**`,
51428
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
51429
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
51430
+ ]
51431
+ };
51432
+ }
50556
51433
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50557
51434
  return new KiroRule({
50558
51435
  outputRoot,
@@ -51403,11 +52280,12 @@ var RooRule = class RooRule extends ToolRule {
51403
52280
  static getSettablePaths(_options = {}) {
51404
52281
  return { nonRoot: { relativeDirPath: buildToolPath(ROO_DIR, "rules", _options.excludeToolDir) } };
51405
52282
  }
51406
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
51407
- const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().nonRoot.relativeDirPath, relativeFilePath));
52283
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true }) {
52284
+ const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : this.getSettablePaths().nonRoot.relativeDirPath;
52285
+ const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
51408
52286
  return new RooRule({
51409
52287
  outputRoot,
51410
- relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
52288
+ relativeDirPath,
51411
52289
  relativeFilePath,
51412
52290
  fileContent,
51413
52291
  validate,
@@ -51415,12 +52293,16 @@ var RooRule = class RooRule extends ToolRule {
51415
52293
  });
51416
52294
  }
51417
52295
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
51418
- return new RooRule(this.buildToolRuleParamsDefault({
52296
+ const params = this.buildToolRuleParamsDefault({
51419
52297
  outputRoot,
51420
52298
  rulesyncRule,
51421
52299
  validate,
51422
52300
  nonRootPath: this.getSettablePaths().nonRoot
51423
- }));
52301
+ });
52302
+ const mode = rulesyncRule.getFrontmatter().roo?.mode;
52303
+ if (!params.root && mode !== void 0 && mode !== "") if (!ROO_MODE_SLUG_PATTERN.test(mode)) warnWithFallback(void 0, `Ignoring roo.mode "${mode}" on ${rulesyncRule.getRelativeFilePath()}: a mode slug may contain only letters, digits and hyphens. Writing the rule to ${params.relativeDirPath} instead.`);
52304
+ else params.relativeDirPath = join(dirname(params.relativeDirPath), rooModeRulesDirName(mode));
52305
+ return new RooRule(params);
51424
52306
  }
51425
52307
  /**
51426
52308
  * Extract mode slug from file path for mode-specific rules
@@ -51432,8 +52314,51 @@ var RooRule = class RooRule extends ToolRule {
51432
52314
  const singleFileMatch = filePath.match(/\.(roo|cline)rules-([a-zA-Z0-9-]+)$/);
51433
52315
  if (singleFileMatch) return singleFileMatch[2];
51434
52316
  }
52317
+ /**
52318
+ * The mode slug of a `.roo/rules-{mode}/` directory, or `undefined` for the
52319
+ * generic `.roo/rules/` directory and anything else. Path-shaped input is
52320
+ * rejected by the slug pattern, so a crafted directory name cannot be lifted
52321
+ * back into frontmatter.
52322
+ */
52323
+ static extractModeFromDirPath(relativeDirPath) {
52324
+ for (const segment of toPosixPath(relativeDirPath).split("/")) {
52325
+ if (!segment.startsWith("rules-")) continue;
52326
+ const mode = segment.slice(6);
52327
+ if (ROO_MODE_SLUG_PATTERN.test(mode)) return mode;
52328
+ }
52329
+ }
51435
52330
  toRulesyncRule() {
51436
- return this.toRulesyncRuleDefault();
52331
+ const mode = RooRule.extractModeFromDirPath(this.getRelativeDirPath());
52332
+ if (mode === void 0) return this.toRulesyncRuleDefault();
52333
+ const baseName = this.getRelativeFilePath().replace(/\.md$/, "");
52334
+ const suffix = `-${mode}`;
52335
+ const importedName = baseName.endsWith(suffix) ? baseName : `${baseName}${suffix}`;
52336
+ return new RulesyncRule({
52337
+ outputRoot: this.getOutputRoot(),
52338
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
52339
+ relativeFilePath: `${importedName}.md`,
52340
+ frontmatter: {
52341
+ root: false,
52342
+ targets: [this.constructor.getToolTargetName()],
52343
+ description: this.description,
52344
+ globs: this.globs ?? [],
52345
+ roo: { mode }
52346
+ },
52347
+ body: this.getFileContent(),
52348
+ validate: true
52349
+ });
52350
+ }
52351
+ /**
52352
+ * Mode-specific rule directories (`.roo/rules-{mode}/`), which Roo/Zoo Code
52353
+ * load instead of `.roo/rules/` while that mode is active. Import-only: the
52354
+ * generic directory is the only one the deletion sweep enumerates, because a
52355
+ * `rules-*` glob would also match mode rules a user wrote by hand.
52356
+ */
52357
+ static getNestedFilePatterns({ outputRoot }) {
52358
+ return {
52359
+ include: [`${toPosixPath(outputRoot)}/${toPosixPath(ROO_DIR)}/rules-*/**/*.md`],
52360
+ ignore: []
52361
+ };
51437
52362
  }
51438
52363
  validate() {
51439
52364
  return {
@@ -51454,10 +52379,18 @@ var RooRule = class RooRule extends ToolRule {
51454
52379
  static isTargetedByRulesyncRule(rulesyncRule) {
51455
52380
  return this.isTargetedByRulesyncRuleDefault({
51456
52381
  rulesyncRule,
51457
- toolTarget: "roo"
52382
+ toolTarget: this.getToolTargetName()
51458
52383
  });
51459
52384
  }
51460
52385
  /**
52386
+ * The tool target this class imports as. `ZoocodeRule` narrows the same
52387
+ * `.roo/` adapters to the `zoocode` target, so the name has to come from the
52388
+ * class rather than a literal.
52389
+ */
52390
+ static getToolTargetName() {
52391
+ return "roo";
52392
+ }
52393
+ /**
51461
52394
  * Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
51462
52395
  * at the project root, not under `.roo/` (mirrors rovodev).
51463
52396
  */
@@ -51990,11 +52923,8 @@ var ZedRule = class ZedRule extends ToolRule {
51990
52923
  * @see https://docs.zoocode.dev
51991
52924
  */
51992
52925
  var ZoocodeRule = class extends RooRule {
51993
- static isTargetedByRulesyncRule(rulesyncRule) {
51994
- return this.isTargetedByRulesyncRuleDefault({
51995
- rulesyncRule,
51996
- toolTarget: "zoocode"
51997
- });
52926
+ static getToolTargetName() {
52927
+ return "zoocode";
51998
52928
  }
51999
52929
  };
52000
52930
  //#endregion
@@ -52021,6 +52951,18 @@ const resolveIncludeLocalRoot = (options) => {
52021
52951
  return parsed.data.includeLocalRoot ?? true;
52022
52952
  };
52023
52953
  /**
52954
+ * Whether `candidate` is `base` itself or a class that extends it.
52955
+ *
52956
+ * Class-identity dispatch (`factory.class === Base`) is subclass-blind: a target
52957
+ * whose adapter merely narrows another one — `ZoocodeRule extends RooRule` — fails
52958
+ * a strict `===` and silently falls through. That is how `zoocode` ended up
52959
+ * declaring `localRootMode: "separate-local-file"` while never emitting
52960
+ * `AGENTS.local.md`, in both directions and without a warning.
52961
+ */
52962
+ function isClassOrSubclassOf({ candidate, base }) {
52963
+ return candidate === base || Object.prototype.isPrototypeOf.call(base, candidate);
52964
+ }
52965
+ /**
52024
52966
  * Factory Map mapping tool targets to their rule factories.
52025
52967
  * Using Map to preserve insertion order for consistent iteration.
52026
52968
  */
@@ -52374,7 +53316,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
52374
53316
  meta: {
52375
53317
  extension: "md",
52376
53318
  supportsGlobal: true,
52377
- ruleDiscoveryMode: "auto"
53319
+ ruleDiscoveryMode: "auto",
53320
+ localRootMode: "separate-local-file",
53321
+ localRootFileName: "AGENTS.local.md"
52378
53322
  }
52379
53323
  }],
52380
53324
  ["zed", {
@@ -52661,7 +53605,10 @@ var RulesProcessor = class extends FeatureProcessor {
52661
53605
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
52662
53606
  }
52663
53607
  buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
52664
- if (factory.class === ClaudecodeRule) {
53608
+ if (isClassOrSubclassOf({
53609
+ candidate: factory.class,
53610
+ base: ClaudecodeRule
53611
+ })) {
52665
53612
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
52666
53613
  return new ClaudecodeRule({
52667
53614
  outputRoot: this.outputRoot,
@@ -52674,7 +53621,10 @@ var RulesProcessor = class extends FeatureProcessor {
52674
53621
  localRoot
52675
53622
  });
52676
53623
  }
52677
- if (factory.class === ClaudecodeLegacyRule) {
53624
+ if (isClassOrSubclassOf({
53625
+ candidate: factory.class,
53626
+ base: ClaudecodeLegacyRule
53627
+ })) {
52678
53628
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
52679
53629
  return new ClaudecodeLegacyRule({
52680
53630
  outputRoot: this.outputRoot,
@@ -52686,7 +53636,10 @@ var RulesProcessor = class extends FeatureProcessor {
52686
53636
  localRoot
52687
53637
  });
52688
53638
  }
52689
- if (factory.class === RovodevRule) return new RovodevRule({
53639
+ if (isClassOrSubclassOf({
53640
+ candidate: factory.class,
53641
+ base: RovodevRule
53642
+ })) return new RovodevRule({
52690
53643
  outputRoot: this.outputRoot,
52691
53644
  relativeDirPath: relativeDirPath ?? ".",
52692
53645
  relativeFilePath: fileName,
@@ -52695,7 +53648,10 @@ var RulesProcessor = class extends FeatureProcessor {
52695
53648
  root: true,
52696
53649
  localRoot
52697
53650
  });
52698
- if (factory.class === RooRule) return new RooRule({
53651
+ if (isClassOrSubclassOf({
53652
+ candidate: factory.class,
53653
+ base: RooRule
53654
+ })) return new RooRule({
52699
53655
  outputRoot: this.outputRoot,
52700
53656
  relativeDirPath: relativeDirPath ?? ".",
52701
53657
  relativeFilePath: fileName,
@@ -52704,7 +53660,23 @@ var RulesProcessor = class extends FeatureProcessor {
52704
53660
  root: true,
52705
53661
  localRoot
52706
53662
  });
52707
- if (factory.class === QwencodeRule) return new QwencodeRule({
53663
+ if (isClassOrSubclassOf({
53664
+ candidate: factory.class,
53665
+ base: DevinRule
53666
+ })) return new DevinRule({
53667
+ outputRoot: this.outputRoot,
53668
+ relativeDirPath: relativeDirPath ?? ".",
53669
+ relativeFilePath: fileName,
53670
+ frontmatter: {},
53671
+ body,
53672
+ validate: true,
53673
+ root: true,
53674
+ localRoot
53675
+ });
53676
+ if (isClassOrSubclassOf({
53677
+ candidate: factory.class,
53678
+ base: QwencodeRule
53679
+ })) return new QwencodeRule({
52708
53680
  outputRoot: this.outputRoot,
52709
53681
  relativeDirPath: relativeDirPath ?? ".qwen",
52710
53682
  relativeFilePath: fileName,
@@ -54921,4 +55893,4 @@ async function importChecksCore(params) {
54921
55893
  //#endregion
54922
55894
  export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
54923
55895
 
54924
- //# sourceMappingURL=import-zGCKgpdt.js.map
55896
+ //# sourceMappingURL=import-Ds0QeVp6.js.map