rulesync 16.12.0 → 16.13.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,6 +412,7 @@ const permissionsProcessorToolTargetTuple = [
412
412
  "kiro-cli",
413
413
  "kiro-ide",
414
414
  "opencode",
415
+ "pi",
415
416
  "qwencode",
416
417
  "reasonix",
417
418
  "rovodev",
@@ -1903,7 +1904,7 @@ function isPlainObject$1(value) {
1903
1904
  /**
1904
1905
  * Type guard to check if a value is an array of strings.
1905
1906
  */
1906
- function isStringArray$1(value) {
1907
+ function isStringArray$2(value) {
1907
1908
  return Array.isArray(value) && value.every((item) => typeof item === "string");
1908
1909
  }
1909
1910
  //#endregion
@@ -2271,6 +2272,7 @@ const HOOK_EVENTS = [
2271
2272
  "todoCreated",
2272
2273
  "todoCompleted",
2273
2274
  "stopFailure",
2275
+ "stopCancelled",
2274
2276
  "instructionsLoaded",
2275
2277
  "userPromptExpansion",
2276
2278
  "postToolBatch",
@@ -2846,6 +2848,7 @@ const GROKCLI_HOOK_EVENTS = [
2846
2848
  "permissionDenied",
2847
2849
  "stop",
2848
2850
  "stopFailure",
2851
+ "stopCancelled",
2849
2852
  "notification",
2850
2853
  "subagentStart",
2851
2854
  "subagentStop",
@@ -2921,16 +2924,10 @@ const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CAN
2921
2924
  /**
2922
2925
  * Hook events supported by Hermes Agent's native Shell Hooks system.
2923
2926
  *
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`.
2927
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
2928
+ * of v0.20.2 (`v2026.8.16`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
2929
+ * full list. Only the events with a clean 1:1 canonical equivalent are mapped
2930
+ * here. All other native events round-trip through `hermesagent.hooks`.
2934
2931
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
2935
2932
  */
2936
2933
  const HERMESAGENT_HOOK_EVENTS = [
@@ -2943,6 +2940,21 @@ const HERMESAGENT_HOOK_EVENTS = [
2943
2940
  "subagentStart",
2944
2941
  "subagentStop"
2945
2942
  ];
2943
+ /**
2944
+ * Hermes Agent's native `VALID_HOOKS` set, in upstream source order.
2945
+ *
2946
+ * The authority is `hermes_cli/plugins.py`'s `VALID_HOOKS`, not the docs: at
2947
+ * v0.20.2 `website/docs/user-guide/features/plugins.md` says "26 lifecycle
2948
+ * events" and then lists 30, omitting the five `on_kanban_*` worker/task/dispatch
2949
+ * observers, `gateway_platform_event` and `transform_api_error_classification`.
2950
+ * Shell hooks are gated on the same set.
2951
+ *
2952
+ * Verified against the tag `v2026.8.16` (v0.20.2): `VALID_HOOKS` holds 37
2953
+ * entries, grown from 23 at `v2026.8.3` (v0.20.0). This list holds 36 of them —
2954
+ * see {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
2955
+ *
2956
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py
2957
+ */
2946
2958
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2947
2959
  "pre_tool_call",
2948
2960
  "post_tool_call",
@@ -2951,6 +2963,10 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2951
2963
  "transform_llm_output",
2952
2964
  "pre_llm_call",
2953
2965
  "post_llm_call",
2966
+ "on_stream_start",
2967
+ "on_stream_delta",
2968
+ "on_stream_end",
2969
+ "on_interim_message",
2954
2970
  "pre_verify",
2955
2971
  "pre_api_request",
2956
2972
  "post_api_request",
@@ -2959,16 +2975,39 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2959
2975
  "on_session_end",
2960
2976
  "on_session_finalize",
2961
2977
  "on_session_reset",
2978
+ "on_skill_lifecycle",
2962
2979
  "subagent_start",
2963
2980
  "subagent_stop",
2964
2981
  "pre_gateway_dispatch",
2965
2982
  "pre_approval_request",
2966
2983
  "post_approval_response",
2984
+ "pre_transcription",
2967
2985
  "kanban_task_claimed",
2968
2986
  "kanban_task_completed",
2969
- "kanban_task_blocked"
2987
+ "kanban_task_blocked",
2988
+ "on_kanban_worker_spawned",
2989
+ "on_kanban_worker_exited",
2990
+ "on_kanban_worker_stale_claim",
2991
+ "on_kanban_task_updated",
2992
+ "on_kanban_dispatch_tick",
2993
+ "gateway_platform_event",
2994
+ "pre_command"
2970
2995
  ];
2971
2996
  /**
2997
+ * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
2998
+ * as the shell-hook allow-list, but `SHELL_UNSUPPORTED_HOOKS` is subtracted from
2999
+ * it first: shell hooks cannot return these events' directives, so
3000
+ * `_parse_hooks_block` refuses the registration with a warning rather than
3001
+ * accepting it and dropping the return value.
3002
+ *
3003
+ * rulesync only ever writes shell hooks, so these are excluded from
3004
+ * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
3005
+ *
3006
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
3007
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/agent/shell_hooks.py — `_parse_hooks_block`
3008
+ */
3009
+ const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
3010
+ /**
2972
3011
  * Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
2973
3012
  * snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
2974
3013
  */
@@ -3585,6 +3624,7 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
3585
3624
  permissionDenied: "PermissionDenied",
3586
3625
  stop: "Stop",
3587
3626
  stopFailure: "StopFailure",
3627
+ stopCancelled: "StopCancelled",
3588
3628
  notification: "Notification",
3589
3629
  subagentStart: "SubagentStart",
3590
3630
  subagentStop: "SubagentStop",
@@ -3859,6 +3899,25 @@ const McpServerSchema = z.looseObject({
3859
3899
  kiroAutoApprove: z.optional(z.array(z.string())),
3860
3900
  kiroAutoBlock: z.optional(z.array(z.string())),
3861
3901
  headers: z.optional(z.record(z.string(), z.string())),
3902
+ /**
3903
+ * The canonical per-server tool allowlist.
3904
+ *
3905
+ * **Collision rule for adapters.** Several tools spell an allowlist natively
3906
+ * under a name that also exists here, so a single server entry can carry both
3907
+ * a canonical field and its native counterpart. What the *native* key means
3908
+ * decides the resolution — not which one was written first:
3909
+ *
3910
+ * - Native key is a redundant spelling of the same concept, and both lists are
3911
+ * additive → **merge** them, so a config that already spells the field
3912
+ * natively keeps working (`kiroAutoApprove`/`kiroAutoBlock` in
3913
+ * `kiro-mcp.ts`).
3914
+ * - Native key is the same concept but rulesync owns the generated value, so
3915
+ * keeping both would be ambiguous → the **canonical value wins** and the
3916
+ * native one is dropped with a warning (`tools` in `copilotcli-mcp.ts`).
3917
+ * - Native key of the same name means something else entirely → **refuse the
3918
+ * canonical value** rather than write a shape the tool misreads (`tools` in
3919
+ * `codexcli-mcp.ts`, where Codex reads it as a per-tool approval table).
3920
+ */
3862
3921
  enabledTools: z.optional(z.array(z.string())),
3863
3922
  disabledTools: z.optional(z.array(z.string()))
3864
3923
  });
@@ -4701,8 +4760,65 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4701
4760
  dangerouslyAllowAll: z.optional(z.boolean())
4702
4761
  });
4703
4762
  /**
4763
+ * Tool-scoped override block for Pi Coding Agent. Pi has no allow/ask/deny rule
4764
+ * surface: the only repository-syncable tool gate is `defaultTools`, the list of
4765
+ * built-in tools enabled at startup (added in v0.84.2). It is an enable-list
4766
+ * rather than a canonical allow/deny rule set, so it is authored here rather
4767
+ * than mapped onto `permission`, mirroring `KimiCodePermissionsOverrideSchema`'s
4768
+ * `tools.enabled` and `VibePermissionsOverrideSchema`'s `enabled_tools`.
4769
+ *
4770
+ * An empty array is meaningful upstream — it starts Pi with no built-in tools
4771
+ * while keeping extension and SDK custom tools — so it is emitted as written.
4772
+ * CLI flags (`--tools`, `--no-tools`, `--no-builtin-tools`, `--exclude-tools`)
4773
+ * outrank the setting, and a project array **replaces** the global one rather
4774
+ * than merging with it.
4775
+ *
4776
+ * @example
4777
+ * { "defaultTools": ["bash", "edit", "write"] }
4778
+ * @see https://pi.dev/docs/latest/settings
4779
+ */
4780
+ const PiPermissionsOverrideSchema = z.looseObject({
4781
+ permission: z.optional(ToolScopedPermissionSchema),
4782
+ defaultTools: z.optional(z.array(z.string()))
4783
+ });
4784
+ /**
4785
+ * The Antigravity CLI's four documented `toolPermission` autonomy presets.
4786
+ * Exported so the import side of `antigravity-cli-permissions.ts` filters
4787
+ * against exactly the values the enum below accepts — an unrecognized value
4788
+ * carried in verbatim would fail validation of the *whole*
4789
+ * `.rulesync/permissions.jsonc`, not just that key.
4790
+ * @see https://antigravity.google/docs/cli/reference
4791
+ */
4792
+ const ANTIGRAVITY_CLI_TOOL_PERMISSIONS = [
4793
+ "request-review",
4794
+ "proceed-in-sandbox",
4795
+ "always-proceed",
4796
+ "strict"
4797
+ ];
4798
+ /**
4799
+ * The Antigravity CLI's three documented `artifactReviewPolicy` values. Shared
4800
+ * with the import filter for the same reason as
4801
+ * {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4802
+ * @see https://antigravity.google/docs/cli/settings
4803
+ */
4804
+ const ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES = [
4805
+ "asks-for-review",
4806
+ "agent-decides",
4807
+ "always-proceed"
4808
+ ];
4809
+ /**
4810
+ * The baseline execution modes the CLI persists as `agentMode`. Shared with the
4811
+ * import filter for the same reason as {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4812
+ * @see https://antigravity.google/docs/cli/modes
4813
+ */
4814
+ const ANTIGRAVITY_CLI_AGENT_MODES = [
4815
+ "default",
4816
+ "accept-edits",
4817
+ "plan"
4818
+ ];
4819
+ /**
4704
4820
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4705
- * `settings.json` carries four global autonomy/sandbox knobs outside the
4821
+ * `settings.json` carries five global autonomy/sandbox knobs outside the
4706
4822
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4707
4823
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4708
4824
  * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
@@ -4710,7 +4826,8 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4710
4826
  * agent's artifact changes are gated on a review prompt — `asks-for-review`
4711
4827
  * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4712
4828
  * (a boolean, off by default, letting the agent read or write files outside the
4713
- * active workspace roots). Antigravity applies the allow/deny
4829
+ * active workspace roots), and `agentMode` (the baseline execution mode a session
4830
+ * starts in — `default` / `accept-edits` / `plan`). Antigravity applies the allow/deny
4714
4831
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4715
4832
  * authors these keys verbatim — no precedence modeling is needed on our side.
4716
4833
  * Fields placed here are merged onto the top level of
@@ -4718,27 +4835,27 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4718
4835
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4719
4836
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4720
4837
  * Verified against https://antigravity.google/docs/cli/reference,
4721
- * https://antigravity.google/docs/cli/sandbox and
4722
- * https://antigravity.google/docs/cli/settings.
4838
+ * https://antigravity.google/docs/cli/sandbox,
4839
+ * https://antigravity.google/docs/cli/settings and
4840
+ * https://antigravity.google/docs/cli/modes.
4723
4841
  *
4724
4842
  * @example
4725
4843
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
4726
4844
  */
4727
4845
  const AntigravityCliPermissionsOverrideSchema = z.looseObject({
4728
4846
  permission: z.optional(ToolScopedPermissionSchema),
4729
- toolPermission: z.optional(z.enum([
4730
- "request-review",
4731
- "proceed-in-sandbox",
4732
- "always-proceed",
4733
- "strict"
4734
- ])),
4847
+ toolPermission: z.optional(z.enum(ANTIGRAVITY_CLI_TOOL_PERMISSIONS)),
4735
4848
  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())
4849
+ artifactReviewPolicy: z.optional(z.enum(ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES)),
4850
+ allowNonWorkspaceAccess: z.optional(z.boolean()),
4851
+ /**
4852
+ * The baseline execution mode the CLI starts a session in (Agent Mode), the
4853
+ * persisted form of the `/settings` panel's Agent Mode row. Only
4854
+ * `~/.gemini/antigravity-cli/settings.json` is read for it, which is why this
4855
+ * override stays global-only.
4856
+ * @see https://antigravity.google/docs/cli/modes
4857
+ */
4858
+ agentMode: z.optional(z.enum(ANTIGRAVITY_CLI_AGENT_MODES))
4742
4859
  });
4743
4860
  /**
4744
4861
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -5048,6 +5165,7 @@ const PermissionsConfigSchema = z.looseObject({
5048
5165
  takt: z.optional(TaktPermissionsOverrideSchema),
5049
5166
  amp: z.optional(AmpPermissionsOverrideSchema),
5050
5167
  "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema),
5168
+ pi: z.optional(PiPermissionsOverrideSchema),
5051
5169
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
5052
5170
  kiro: z.optional(KiroPermissionsOverrideSchema),
5053
5171
  codexcli: z.optional(CodexcliPermissionsOverrideSchema),
@@ -5529,6 +5647,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
5529
5647
  cursor: z.optional(z.looseObject({
5530
5648
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
5531
5649
  "disable-model-invocation": z.optional(z.boolean()),
5650
+ "user-invocable": z.optional(z.boolean()),
5532
5651
  metadata: z.optional(z.looseObject({}))
5533
5652
  })),
5534
5653
  factorydroid: z.optional(z.looseObject({
@@ -5753,6 +5872,11 @@ async function getLocalSkillDirNames(outputRoot) {
5753
5872
  * default with a per-target value. A defined section value (including `false`)
5754
5873
  * always wins over the root default.
5755
5874
  *
5875
+ * `devin` also consumes the root-level value — it maps `true` onto a user-only
5876
+ * `triggers` list (see `devin-skill.ts`) — but has no section key of the same
5877
+ * name, so it does not go through this helper. A `devin.triggers` section value
5878
+ * still overrides it.
5879
+ *
5756
5880
  * @returns The resolved boolean, or `undefined` when neither value is set.
5757
5881
  */
5758
5882
  function resolveDisableModelInvocation({ rootFrontmatter, section }) {
@@ -5763,10 +5887,15 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
5763
5887
  *
5764
5888
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
5765
5889
  * 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
5890
+ * copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5767
5891
  * per-target value. A defined section value (including `false`) always wins
5768
5892
  * over the root default.
5769
5893
  *
5894
+ * `devin` also consumes the root-level value — it maps `false` onto a
5895
+ * model-only `triggers` list (see `devin-skill.ts`) — but has no section key of
5896
+ * the same name, so it does not go through this helper. A `devin.triggers`
5897
+ * section value still overrides it.
5898
+ *
5770
5899
  * @returns The resolved boolean, or `undefined` when neither value is set.
5771
5900
  */
5772
5901
  function resolveUserInvocable({ rootFrontmatter, section }) {
@@ -7616,6 +7745,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7616
7745
  }
7617
7746
  }
7618
7747
  },
7748
+ ".pi/settings.json": {
7749
+ format: "json",
7750
+ invalidRootPolicy: "error",
7751
+ features: { permissions: {
7752
+ kind: "replace-owned-keys",
7753
+ ownedKeys: ["defaultTools"]
7754
+ } }
7755
+ },
7756
+ ".pi/agent/settings.json": {
7757
+ format: "json",
7758
+ invalidRootPolicy: "error",
7759
+ features: { permissions: {
7760
+ kind: "replace-owned-keys",
7761
+ ownedKeys: ["defaultTools"]
7762
+ } }
7763
+ },
7619
7764
  ".github/copilot/settings.json": {
7620
7765
  format: "json",
7621
7766
  invalidRootPolicy: "error",
@@ -12202,6 +12347,8 @@ const PI_RULE_FILE_NAME = "AGENTS.md";
12202
12347
  const PI_RULE_OVERRIDE_FILE_NAME = "AGENTS.override.md";
12203
12348
  const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
12204
12349
  const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
12350
+ const PI_SETTINGS_FILE_NAME = "settings.json";
12351
+ const PI_AGENT_DIR_PATH = PI_AGENT_DIR;
12205
12352
  //#endregion
12206
12353
  //#region src/features/commands/pi-command.ts
12207
12354
  /**
@@ -14126,7 +14273,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
14126
14273
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
14127
14274
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
14128
14275
  const isEmittableString = ({ value }) => isNonEmptyString(value);
14129
- const isEmittableArray = ({ value }) => isStringArray(value);
14276
+ const isEmittableArray = ({ value }) => isStringArray$1(value);
14130
14277
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
14131
14278
  const isImportableArray = ({ value }) => isSafeStringArray(value);
14132
14279
  /**
@@ -14435,7 +14582,7 @@ function isStringRecord(value) {
14435
14582
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
14436
14583
  return Object.values(value).every((v) => typeof v === "string");
14437
14584
  }
14438
- function isStringArray(value) {
14585
+ function isStringArray$1(value) {
14439
14586
  return Array.isArray(value) && value.every((v) => typeof v === "string");
14440
14587
  }
14441
14588
  /** Compare object values without letting key order decide the answer. */
@@ -14467,7 +14614,7 @@ function isSafeStringRecord(value) {
14467
14614
  * skipped wholesale when that read fails.
14468
14615
  */
14469
14616
  function isSafeStringArray(value) {
14470
- return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14617
+ return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14471
14618
  }
14472
14619
  /**
14473
14620
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -14509,7 +14656,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
14509
14656
  return {
14510
14657
  ...url !== void 0 && { url },
14511
14658
  ...isStringRecord(headers) && { headers },
14512
- ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14659
+ ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14513
14660
  };
14514
14661
  }
14515
14662
  if (hookType === "mcp_tool") {
@@ -15492,7 +15639,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
15492
15639
  function parseManifest(fileContent) {
15493
15640
  try {
15494
15641
  const parsed = JSON.parse(fileContent);
15495
- if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
15642
+ if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
15496
15643
  return {
15497
15644
  generatedBy: "rulesync",
15498
15645
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -17127,6 +17274,7 @@ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "po
17127
17274
  const HERMESAGENT_FAIL_CLOSED_EVENT = "pre_tool_call";
17128
17275
  const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
17129
17276
  const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
17277
+ const HERMESAGENT_SHELL_UNSUPPORTED_EVENTS = new Set(HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS);
17130
17278
  /**
17131
17279
  * Whether an entry of the `hooks:` mapping is a hook-event list rather than one
17132
17280
  * of its non-event siblings.
@@ -17215,7 +17363,8 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
17215
17363
  }
17216
17364
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
17217
17365
  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.`);
17366
+ 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.`);
17367
+ 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
17368
  setHermesHookEntries({
17220
17369
  result,
17221
17370
  event: nativeEvent,
@@ -22339,7 +22488,7 @@ function isCodexStdioServer(config) {
22339
22488
  * @see https://learn.chatgpt.com/docs/extend/mcp
22340
22489
  */
22341
22490
  function isValidRenamedArray(key, value) {
22342
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
22491
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
22343
22492
  }
22344
22493
  /**
22345
22494
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
@@ -22859,6 +23008,49 @@ const resolveCopilotcliServerType = (server) => {
22859
23008
  return isRemoteMcpServer(server) ? "http" : "stdio";
22860
23009
  };
22861
23010
  /**
23011
+ * Copilot CLI's per-server tool allowlist. `tools` selects which of the
23012
+ * server's tools are exposed — `["*"]` (the default) for all, or a list of
23013
+ * names — so the canonical `enabledTools` is written under that name rather
23014
+ * than passed through as a key the CLI ignores.
23015
+ *
23016
+ * When a server carries both, the canonical `enabledTools` wins and the native
23017
+ * `tools` is dropped with a warning. rulesync owns the generated allowlist, and
23018
+ * the `tools` value most likely present is the upstream default `["*"]` — left
23019
+ * to win, it would silently discard a real allowlist in favour of "expose every
23020
+ * tool", which is the failure this mapping exists to prevent. That is the
23021
+ * "canonical wins" branch of the collision rule documented on `enabledTools` in
23022
+ * `src/types/mcp.ts`; `kiro-mcp.ts` merges instead (its native names are
23023
+ * redundant spellings of additive lists) and `codexcli-mcp.ts` refuses instead
23024
+ * (Codex's same-named key means a different thing).
23025
+ *
23026
+ * `disabledTools` has no counterpart upstream (expressing it would need the
23027
+ * server's full tool list), so the target keeps `supportsDisabledTools: false`
23028
+ * and the processor strips it before it reaches here.
23029
+ *
23030
+ * @see https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
23031
+ */
23032
+ function renameEnabledToolsToTools({ server, serverName, logger }) {
23033
+ const { enabledTools, ...rest } = server;
23034
+ if (enabledTools === void 0) return rest;
23035
+ if (rest.tools !== void 0) logger?.warn(`[CopilotcliMcp] MCP server "${serverName}" declares both 'tools' and 'enabledTools'; keeping 'enabledTools' and dropping 'tools'.`);
23036
+ return {
23037
+ ...rest,
23038
+ tools: enabledTools
23039
+ };
23040
+ }
23041
+ /**
23042
+ * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
23043
+ * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
23044
+ * CLI-written entry can be a bare string. Canonical `enabledTools` is
23045
+ * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
23046
+ * through would reject the WHOLE imported MCP file rather than one key. Anything
23047
+ * that is not a string array is therefore left under `tools` untouched, matching
23048
+ * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
23049
+ */
23050
+ function isStringArray(value) {
23051
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
23052
+ }
23053
+ /**
22862
23054
  * Resolves and sets the transport type for each MCP server config.
22863
23055
  * GitHub Copilot CLI requires the "type" field for each server. A server it
22864
23056
  * cannot express — one that names no transport, or names one it carries no way
@@ -22902,7 +23094,11 @@ function addTypeField(mcpServers, logger) {
22902
23094
  }
22903
23095
  const { httpUrl: _httpUrl, ...rest } = parsed;
22904
23096
  result[name] = {
22905
- ...rest,
23097
+ ...renameEnabledToolsToTools({
23098
+ server: rest,
23099
+ serverName: name,
23100
+ logger
23101
+ }),
22906
23102
  type,
22907
23103
  url
22908
23104
  };
@@ -22920,7 +23116,11 @@ function addTypeField(mcpServers, logger) {
22920
23116
  continue;
22921
23117
  }
22922
23118
  result[name] = {
22923
- ...local,
23119
+ ...renameEnabledToolsToTools({
23120
+ server: local,
23121
+ serverName: name,
23122
+ logger
23123
+ }),
22924
23124
  type,
22925
23125
  command: head,
22926
23126
  ...tail.length > 0 && { args: tail }
@@ -22934,11 +23134,19 @@ function addTypeField(mcpServers, logger) {
22934
23134
  function removeTypeField(config) {
22935
23135
  const result = {};
22936
23136
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
22937
- if (server.type !== "stdio") {
22938
- result[name] = server;
23137
+ const { tools, ...withoutTools } = server;
23138
+ const restored = isStringArray(tools) ? {
23139
+ ...withoutTools,
23140
+ enabledTools: tools
23141
+ } : {
23142
+ ...withoutTools,
23143
+ ...tools !== void 0 && { tools }
23144
+ };
23145
+ if (restored.type !== "stdio") {
23146
+ result[name] = restored;
22939
23147
  continue;
22940
23148
  }
22941
- const { type: _, ...rest } = server;
23149
+ const { type: _, ...rest } = restored;
22942
23150
  result[name] = rest;
22943
23151
  }
22944
23152
  return result;
@@ -23590,11 +23798,11 @@ function applyGooseStdioFields(ext, config) {
23590
23798
  if (Array.isArray(command)) {
23591
23799
  if (typeof command[0] === "string") ext.cmd = command[0];
23592
23800
  const rest = command.slice(1).filter((c) => typeof c === "string");
23593
- const args = isStringArray$1(config.args) ? config.args : [];
23801
+ const args = isStringArray$2(config.args) ? config.args : [];
23594
23802
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
23595
23803
  } else if (typeof command === "string") {
23596
23804
  ext.cmd = command;
23597
- if (isStringArray$1(config.args)) ext.args = config.args;
23805
+ if (isStringArray$2(config.args)) ext.args = config.args;
23598
23806
  }
23599
23807
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
23600
23808
  }
@@ -23696,7 +23904,7 @@ function convertFromGooseFormat(extensions) {
23696
23904
  else if (type === "streamable_http") server.type = "http";
23697
23905
  else server.type = "stdio";
23698
23906
  if (typeof ext.cmd === "string") server.command = ext.cmd;
23699
- if (isStringArray$1(ext.args)) server.args = ext.args;
23907
+ if (isStringArray$2(ext.args)) server.args = ext.args;
23700
23908
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
23701
23909
  if (typeof ext.uri === "string") server.url = ext.uri;
23702
23910
  if (isPlainObject$1(ext.headers)) server.headers = omitPrototypePollutionKeys(ext.headers);
@@ -23717,11 +23925,11 @@ function buildGoosePluginStdioServer(config) {
23717
23925
  if (Array.isArray(command)) {
23718
23926
  if (typeof command[0] === "string") server.command = command[0];
23719
23927
  const rest = command.slice(1).filter((c) => typeof c === "string");
23720
- const args = isStringArray$1(config.args) ? config.args : [];
23928
+ const args = isStringArray$2(config.args) ? config.args : [];
23721
23929
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
23722
23930
  } else if (typeof command === "string") {
23723
23931
  server.command = command;
23724
- if (isStringArray$1(config.args)) server.args = config.args;
23932
+ if (isStringArray$2(config.args)) server.args = config.args;
23725
23933
  }
23726
23934
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
23727
23935
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -24053,7 +24261,7 @@ function copyHermesOauth(source) {
24053
24261
  "client_secret"
24054
24262
  ]) if (typeof source[key] === "string") oauth[key] = source[key];
24055
24263
  if (typeof source.redirect_port === "number") oauth.redirect_port = source.redirect_port;
24056
- if (isStringArray$1(source.scopes)) oauth.scopes = source.scopes;
24264
+ if (isStringArray$2(source.scopes)) oauth.scopes = source.scopes;
24057
24265
  return Object.keys(oauth).length > 0 ? oauth : void 0;
24058
24266
  }
24059
24267
  function copyHermesAdvancedFields(source, target) {
@@ -24062,7 +24270,7 @@ function copyHermesAdvancedFields(source, target) {
24062
24270
  target.auth = source.auth;
24063
24271
  copied = true;
24064
24272
  }
24065
- if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
24273
+ if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
24066
24274
  target.client_cert = source.client_cert;
24067
24275
  copied = true;
24068
24276
  }
@@ -24131,8 +24339,8 @@ function copyHermesAdvancedFields(source, target) {
24131
24339
  */
24132
24340
  function buildHermesToolsBlock(config) {
24133
24341
  const tools = {};
24134
- if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
24135
- if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
24342
+ if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
24343
+ if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
24136
24344
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
24137
24345
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
24138
24346
  return tools;
@@ -24144,8 +24352,8 @@ function buildHermesToolsBlock(config) {
24144
24352
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
24145
24353
  */
24146
24354
  function applyHermesToolsBlock(hermesTools, server) {
24147
- if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
24148
- if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24355
+ if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
24356
+ if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24149
24357
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
24150
24358
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
24151
24359
  }
@@ -24171,11 +24379,11 @@ function convertServerToHermes(config) {
24171
24379
  if (Array.isArray(command)) {
24172
24380
  if (typeof command[0] === "string") out.command = command[0];
24173
24381
  const rest = command.slice(1).filter((c) => typeof c === "string");
24174
- const args = isStringArray$1(config.args) ? config.args : [];
24382
+ const args = isStringArray$2(config.args) ? config.args : [];
24175
24383
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
24176
24384
  } else if (typeof command === "string") {
24177
24385
  out.command = command;
24178
- if (isStringArray$1(config.args)) out.args = config.args;
24386
+ if (isStringArray$2(config.args)) out.args = config.args;
24179
24387
  }
24180
24388
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
24181
24389
  } else if (url !== void 0) {
@@ -24225,7 +24433,7 @@ function convertFromHermesFormat(mcpServers) {
24225
24433
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
24226
24434
  const server = {};
24227
24435
  if (typeof config.command === "string") server.command = config.command;
24228
- if (isStringArray$1(config.args)) server.args = config.args;
24436
+ if (isStringArray$2(config.args)) server.args = config.args;
24229
24437
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
24230
24438
  if (typeof config.url === "string") server.url = config.url;
24231
24439
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -25183,13 +25391,17 @@ function mergeToolLists(...lists) {
25183
25391
  *
25184
25392
  * Both native names are documented per-server fields, so a config that already
25185
25393
  * spells them natively keeps working: the two lists are merged rather than
25186
- * one overwriting the other.
25394
+ * one overwriting the other. That is the "merge" branch of the collision rule
25395
+ * documented on `enabledTools` in `src/types/mcp.ts` — it applies here because
25396
+ * the native names are redundant spellings of the same additive lists;
25397
+ * `copilotcli-mcp.ts` lets the canonical value win instead, and
25398
+ * `codexcli-mcp.ts` refuses the canonical value instead.
25187
25399
  * @see https://kiro.dev/docs/mcp/configuration/
25188
25400
  */
25189
25401
  function toKiroMcpServers(servers) {
25190
25402
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25191
25403
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
25192
- const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25404
+ const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25193
25405
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
25194
25406
  return [name, {
25195
25407
  ...rest,
@@ -25213,7 +25425,7 @@ function fromKiroMcpServers(servers) {
25213
25425
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25214
25426
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
25215
25427
  const { autoApprove, ...rest } = server;
25216
- if (!isStringArray$1(autoApprove)) return [name, server];
25428
+ if (!isStringArray$2(autoApprove)) return [name, server];
25217
25429
  return [name, {
25218
25430
  ...rest,
25219
25431
  kiroAutoApprove: autoApprove
@@ -26355,7 +26567,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
26355
26567
  }
26356
26568
  function disabledNamesOf(config) {
26357
26569
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
26358
- return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26570
+ return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26359
26571
  }
26360
26572
  /**
26361
26573
  * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
@@ -26488,7 +26700,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
26488
26700
  return [];
26489
26701
  }
26490
26702
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
26491
- const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26703
+ const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26492
26704
  const managedNameSet = new Set(managedNames);
26493
26705
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
26494
26706
  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 +27542,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27330
27542
  meta: {
27331
27543
  supportsProject: true,
27332
27544
  supportsGlobal: true,
27333
- supportsEnabledTools: false,
27545
+ supportsEnabledTools: true,
27334
27546
  supportsDisabledTools: false
27335
27547
  }
27336
27548
  }],
@@ -28154,6 +28366,34 @@ function convertAmpToRulesync({ disable, permissions }) {
28154
28366
  //#endregion
28155
28367
  //#region src/features/permissions/antigravity-cli-permissions.ts
28156
28368
  /**
28369
+ * Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
28370
+ * permissions override may author. They are siblings of `permissions`, passed
28371
+ * through verbatim in both directions.
28372
+ */
28373
+ const ANTIGRAVITY_CLI_OVERRIDE_KEYS = [
28374
+ "toolPermission",
28375
+ "enableTerminalSandbox",
28376
+ "artifactReviewPolicy",
28377
+ "allowNonWorkspaceAccess",
28378
+ "agentMode"
28379
+ ];
28380
+ const moduleLogger$2 = fallbackLogger;
28381
+ /**
28382
+ * Narrow an imported `settings.json` value to one of the documented values of
28383
+ * an enum-typed override key. The override schema declares `toolPermission`,
28384
+ * `artifactReviewPolicy` and `agentMode` as `z.enum`, so carrying an
28385
+ * unrecognized value through — a hand-written typo, or a preset a newer
28386
+ * Antigravity release added — would fail validation of the whole imported
28387
+ * `.rulesync/permissions.jsonc` instead of dropping just that key. A dropped
28388
+ * value is warned about rather than discarded silently, so a preset rulesync
28389
+ * does not know about yet is visible instead of quietly missing.
28390
+ */
28391
+ function pickDocumentedValue({ key, value, documented }) {
28392
+ if (typeof value !== "string") return void 0;
28393
+ if (documented.includes(value)) return value;
28394
+ 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.`);
28395
+ }
28396
+ /**
28157
28397
  * Deliberate mapping from rulesync canonical permission categories to the
28158
28398
  * Antigravity CLI Fine-Grained Permissions Engine action vocabulary
28159
28399
  * (`read_file`, `write_file`, `read_url`, `command`, `mcp`, plus `execute_url` /
@@ -28228,9 +28468,10 @@ function buildPermissionEntry$1(toolName, pattern) {
28228
28468
  * file (global scope only). The file holds other CLI settings besides
28229
28469
  * permissions, so it is never deleted.
28230
28470
  *
28231
- * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28471
+ * Five CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28232
28472
  * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
28233
- * `artifactReviewPolicy` and `allowNonWorkspaceAccess` — are authored and
28473
+ * `artifactReviewPolicy`, `allowNonWorkspaceAccess` and `agentMode` (the
28474
+ * baseline execution mode) — are authored and
28234
28475
  * round-tripped through the `antigravity-cli` permissions override (see
28235
28476
  * `AntigravityCliPermissionsOverrideSchema`).
28236
28477
  */
@@ -28294,10 +28535,10 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28294
28535
  permissions: mergedPermissions
28295
28536
  };
28296
28537
  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;
28538
+ for (const key of ANTIGRAVITY_CLI_OVERRIDE_KEYS) {
28539
+ const value = override?.[key];
28540
+ if (value !== void 0) merged[key] = value;
28541
+ }
28301
28542
  const fileContent = JSON.stringify(merged, null, 2);
28302
28543
  return new AntigravityCliPermissions({
28303
28544
  outputRoot,
@@ -28322,10 +28563,26 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28322
28563
  deny: permissions.deny ?? []
28323
28564
  });
28324
28565
  const override = {};
28325
- if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
28566
+ const toolPermission = pickDocumentedValue({
28567
+ key: "toolPermission",
28568
+ value: settings.toolPermission,
28569
+ documented: ANTIGRAVITY_CLI_TOOL_PERMISSIONS
28570
+ });
28571
+ if (toolPermission !== void 0) override.toolPermission = toolPermission;
28326
28572
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
28327
- if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
28573
+ const artifactReviewPolicy = pickDocumentedValue({
28574
+ key: "artifactReviewPolicy",
28575
+ value: settings.artifactReviewPolicy,
28576
+ documented: ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES
28577
+ });
28578
+ if (artifactReviewPolicy !== void 0) override.artifactReviewPolicy = artifactReviewPolicy;
28328
28579
  if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
28580
+ const agentMode = pickDocumentedValue({
28581
+ key: "agentMode",
28582
+ value: settings.agentMode,
28583
+ documented: ANTIGRAVITY_CLI_AGENT_MODES
28584
+ });
28585
+ if (agentMode !== void 0) override.agentMode = agentMode;
28329
28586
  const result = { ...config };
28330
28587
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
28331
28588
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -29151,6 +29408,119 @@ function deepMergeRecords(base, patch) {
29151
29408
  }
29152
29409
  return merged;
29153
29410
  }
29411
+ /**
29412
+ * `sandbox.*` paths Claude Code honors only from user settings, managed settings
29413
+ * and the `--settings` CLI flag. Written into a project `.claude/settings.json`
29414
+ * they are silently ignored, so a committed file would read as though it
29415
+ * enforced a sandbox policy while doing nothing — security-relevant for
29416
+ * `network.strictAllowlist` and the credential-masking keys in particular.
29417
+ * Generation therefore drops them at project scope with a per-key warning and
29418
+ * emits them only under `--global`.
29419
+ *
29420
+ * Deliberately NOT listed:
29421
+ * - `bwrapPath` / `socatPath`: v2.1.232 added them to the managed-settings
29422
+ * approval dialog, which is a consent prompt, not a project-scope rejection.
29423
+ * - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
29424
+ * unit is the individual entry's mode, not the settings key, and the same
29425
+ * lists carry `deny` entries that project settings *do* honor — dropping a
29426
+ * whole list would remove real restrictions. `stripProjectIgnoredMaskEntries`
29427
+ * filters those lists per entry instead.
29428
+ *
29429
+ * @see https://code.claude.com/docs/en/sandboxing
29430
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
29431
+ */
29432
+ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29433
+ ["filesystem", "disabled"],
29434
+ ["network", "strictAllowlist"],
29435
+ ["network", "tlsTerminate"],
29436
+ ["credentials", "allowPlaintextInject"],
29437
+ ["credentials", "awsPairs"],
29438
+ ["credentials", "sigv4"],
29439
+ ["allowAppleEvents"],
29440
+ ["ripgrep"]
29441
+ ];
29442
+ /**
29443
+ * Copy of the authored `sandbox` override with the user/managed-only paths
29444
+ * removed, warning once per dropped path. Only the override copy is filtered —
29445
+ * a value already hand-written in the target file is left untouched, matching
29446
+ * the `qwencode` `security.allowPrivateNetworkHooks` precedent.
29447
+ */
29448
+ function stripGlobalOnlySandboxPaths({ sandbox, relativeFilePath, logger }) {
29449
+ const filtered = structuredClone(sandbox);
29450
+ for (const path of CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS) {
29451
+ const leaf = path.at(-1);
29452
+ if (leaf === void 0) continue;
29453
+ const parentPath = path.slice(0, -1);
29454
+ let parent = filtered;
29455
+ for (const segment of parentPath) {
29456
+ const next = parent[segment];
29457
+ if (!isPlainRecord(next)) {
29458
+ parent = {};
29459
+ break;
29460
+ }
29461
+ parent = next;
29462
+ }
29463
+ if (parent[leaf] === void 0) continue;
29464
+ delete parent[leaf];
29465
+ const [container] = parentPath;
29466
+ if (container !== void 0 && isPlainRecord(filtered[container])) {
29467
+ if (Object.keys(filtered[container]).length === 0) delete filtered[container];
29468
+ }
29469
+ 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.`);
29470
+ }
29471
+ return filtered;
29472
+ }
29473
+ /** The `sandbox.credentials` lists whose entries accept `"mode": "mask"`. */
29474
+ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
29475
+ /**
29476
+ * Copy of the authored `sandbox` override with the `credentials.envVars` /
29477
+ * `credentials.files` entries whose `mode` is `"mask"` removed, warning once per
29478
+ * list.
29479
+ *
29480
+ * Masking authorizes the sandbox proxy to send the real credential to the listed
29481
+ * hosts, so Claude Code honors it only from user settings, managed settings and
29482
+ * the `--settings` CLI flag; a `mask` entry in a repository's
29483
+ * `.claude/settings.json` is ignored outright. Keeping it would read as though
29484
+ * the credential were masked while nothing protects it — the one project-scope
29485
+ * gap whose consequence is a live credential leaving the sandbox unmasked.
29486
+ *
29487
+ * Filtered per entry rather than per key, because the same lists carry `deny`
29488
+ * entries that project settings *do* honor. `mode` is matched exactly: an entry
29489
+ * Claude Code cannot read as `mask` is not treated as one either (it degrades
29490
+ * such entries to `deny`), so the "reads as masked but isn't" state this guards
29491
+ * against cannot slip through a differently-spelled value.
29492
+ *
29493
+ * Like `stripGlobalOnlySandboxPaths`, only the override copy is filtered — a
29494
+ * value already in the target file is left untouched, which is why the warning
29495
+ * points at it.
29496
+ *
29497
+ * @see https://code.claude.com/docs/en/sandboxing — "`mask` entries … are all
29498
+ * ignored in a repository's `.claude/settings.json` or
29499
+ * `.claude/settings.local.json`"; the file-entry section states the same
29500
+ * settings-source restriction applies there.
29501
+ */
29502
+ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
29503
+ const credentials = sandbox.credentials;
29504
+ if (!isPlainRecord(credentials)) return sandbox;
29505
+ const filteredCredentials = { ...credentials };
29506
+ let changed = false;
29507
+ for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
29508
+ const list = filteredCredentials[listKey];
29509
+ if (!Array.isArray(list)) continue;
29510
+ const kept = list.filter((entry) => !(isPlainRecord(entry) && entry.mode === "mask"));
29511
+ if (kept.length === list.length) continue;
29512
+ changed = true;
29513
+ const dropped = list.length - kept.length;
29514
+ 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.`);
29515
+ if (kept.length === 0) delete filteredCredentials[listKey];
29516
+ else filteredCredentials[listKey] = kept;
29517
+ }
29518
+ if (!changed) return sandbox;
29519
+ const filtered = { ...sandbox };
29520
+ if (Object.keys(filteredCredentials).length === 0) delete filtered.credentials;
29521
+ else filtered.credentials = filteredCredentials;
29522
+ return filtered;
29523
+ }
29154
29524
  const CLAUDE_PATH_RULE_ALIASES = {
29155
29525
  Write: "Edit",
29156
29526
  NotebookEdit: "Edit",
@@ -29204,7 +29574,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29204
29574
  validate
29205
29575
  });
29206
29576
  }
29207
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
29577
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
29208
29578
  const paths = ClaudecodePermissions.getSettablePaths();
29209
29579
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
29210
29580
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -29228,7 +29598,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29228
29598
  };
29229
29599
  }
29230
29600
  const overrideSandbox = config.claudecode?.sandbox;
29231
- if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
29601
+ if (isPlainRecord(overrideSandbox)) {
29602
+ const scopedSandbox = global ? overrideSandbox : stripProjectIgnoredMaskEntries({
29603
+ sandbox: stripGlobalOnlySandboxPaths({
29604
+ sandbox: overrideSandbox,
29605
+ relativeFilePath: paths.relativeFilePath,
29606
+ logger
29607
+ }),
29608
+ relativeFilePath: paths.relativeFilePath,
29609
+ logger
29610
+ });
29611
+ if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
29612
+ }
29232
29613
  const managedToolNames = managedClaudeToolNames(config);
29233
29614
  const merged = applyPermissions({
29234
29615
  settings,
@@ -31405,7 +31786,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
31405
31786
  function convertGoosePermissionConfigToRulesync(userPermission) {
31406
31787
  const permission = {};
31407
31788
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
31408
- const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
31789
+ const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
31409
31790
  const action = GOOSE_LIST_TO_ACTION[key];
31410
31791
  for (const toolName of toolNames) {
31411
31792
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
@@ -31698,7 +32079,7 @@ const ACTION_RANK = {
31698
32079
  * (mirrors the Cursor adapter's preservation of unmanaged types).
31699
32080
  */
31700
32081
  function unmanagedEntries(existingPermission, key) {
31701
- return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
32082
+ return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
31702
32083
  }
31703
32084
  /**
31704
32085
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -31741,9 +32122,9 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
31741
32122
  * described more than once resolves to the strictest action.
31742
32123
  */
31743
32124
  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;
32125
+ const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
32126
+ const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
32127
+ const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
31747
32128
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31748
32129
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
31749
32130
  const result = {};
@@ -31925,20 +32306,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
31925
32306
  const permission = clonePermissionBlock(provenance.permission);
31926
32307
  reconcileCommandAllowlist({
31927
32308
  permission,
31928
- commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
32309
+ commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
31929
32310
  });
31930
32311
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
31931
32312
  reconcileNativeDenies({
31932
32313
  permission,
31933
32314
  category: "bash",
31934
- patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
32315
+ patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
31935
32316
  });
31936
32317
  const security = isRecord$1(config.security) ? config.security : {};
31937
32318
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
31938
32319
  reconcileNativeDenies({
31939
32320
  permission,
31940
32321
  category: "webfetch",
31941
- patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
32322
+ patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
31942
32323
  });
31943
32324
  removeEmptyCategories(permission);
31944
32325
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -33366,6 +33747,115 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
33366
33747
  }
33367
33748
  };
33368
33749
  //#endregion
33750
+ //#region src/features/permissions/pi-permissions.ts
33751
+ /** The one `settings.json` key this feature owns. */
33752
+ const DEFAULT_TOOLS_KEY = "defaultTools";
33753
+ /**
33754
+ * Permissions generator for Pi Coding Agent.
33755
+ *
33756
+ * Pi exposes no allow/ask/deny rule surface, so no canonical permission
33757
+ * category maps onto it. Its one repository-syncable tool gate is `defaultTools` —
33758
+ * the built-in tools enabled at startup (v0.84.2) — authored through the `pi`
33759
+ * permissions override.
33760
+ *
33761
+ * Scope semantics are the **opposite** of the union-merge most targets use:
33762
+ * a project `defaultTools` array *replaces* the global array rather than adding
33763
+ * to it, so the two scopes are written independently and never combined.
33764
+ * Project scope writes `.pi/settings.json`; global scope writes
33765
+ * `~/.pi/agent/settings.json`.
33766
+ *
33767
+ * The file is hand-edited and holds many unrelated keys (`theme`,
33768
+ * `defaultModel`, `packages`, `sessionDir`, ...), so writes go through the
33769
+ * shared-config gateway with `defaultTools` as the only owned key, and the file
33770
+ * is never deleted.
33771
+ *
33772
+ * @see https://pi.dev/docs/latest/settings
33773
+ */
33774
+ var PiPermissions = class PiPermissions extends ToolPermissions {
33775
+ constructor(params) {
33776
+ super({
33777
+ ...params,
33778
+ fileContent: params.fileContent ?? "{}"
33779
+ });
33780
+ }
33781
+ /** `settings.json` holds unrelated user settings, so it must not be deleted. */
33782
+ isDeletable() {
33783
+ return false;
33784
+ }
33785
+ static getSettablePaths({ global = false } = {}) {
33786
+ return {
33787
+ relativeDirPath: global ? PI_AGENT_DIR_PATH : ".pi",
33788
+ relativeFilePath: PI_SETTINGS_FILE_NAME
33789
+ };
33790
+ }
33791
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33792
+ const paths = PiPermissions.getSettablePaths({ global });
33793
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
33794
+ return new PiPermissions({
33795
+ outputRoot,
33796
+ relativeDirPath: paths.relativeDirPath,
33797
+ relativeFilePath: paths.relativeFilePath,
33798
+ fileContent,
33799
+ validate,
33800
+ global
33801
+ });
33802
+ }
33803
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
33804
+ const paths = PiPermissions.getSettablePaths({ global });
33805
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
33806
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
33807
+ const defaultTools = rulesyncPermissions.getJson().pi?.defaultTools;
33808
+ const patch = {};
33809
+ if (defaultTools !== void 0) patch[DEFAULT_TOOLS_KEY] = defaultTools;
33810
+ return new PiPermissions({
33811
+ outputRoot,
33812
+ relativeDirPath: paths.relativeDirPath,
33813
+ relativeFilePath: paths.relativeFilePath,
33814
+ fileContent: applySharedConfigPatch({
33815
+ fileKey: sharedConfigFileKey(paths),
33816
+ feature: "permissions",
33817
+ existingContent,
33818
+ patch,
33819
+ filePath
33820
+ }),
33821
+ validate: true,
33822
+ global
33823
+ });
33824
+ }
33825
+ toRulesyncPermissions() {
33826
+ const defaultTools = this.parseSettings()[DEFAULT_TOOLS_KEY];
33827
+ const config = { permission: {} };
33828
+ if (Array.isArray(defaultTools) && defaultTools.every((tool) => typeof tool === "string")) config.pi = { [DEFAULT_TOOLS_KEY]: defaultTools };
33829
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
33830
+ }
33831
+ parseSettings() {
33832
+ try {
33833
+ return parseSharedConfig({
33834
+ format: "json",
33835
+ fileContent: this.getFileContent()
33836
+ });
33837
+ } catch (error) {
33838
+ throw new Error(`Failed to parse Pi settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
33839
+ }
33840
+ }
33841
+ validate() {
33842
+ return {
33843
+ success: true,
33844
+ error: null
33845
+ };
33846
+ }
33847
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
33848
+ return new PiPermissions({
33849
+ outputRoot,
33850
+ relativeDirPath,
33851
+ relativeFilePath,
33852
+ fileContent: "{}",
33853
+ validate: false,
33854
+ global
33855
+ });
33856
+ }
33857
+ };
33858
+ //#endregion
33369
33859
  //#region src/features/permissions/qwencode-permissions.ts
33370
33860
  /**
33371
33861
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -34429,7 +34919,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
34429
34919
  permission[category] ??= {};
34430
34920
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
34431
34921
  }
34432
- if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34922
+ if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34433
34923
  permission.read ??= {};
34434
34924
  permission.read[path] ??= "allow";
34435
34925
  }
@@ -35406,8 +35896,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
35406
35896
  const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
35407
35897
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
35408
35898
  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] : []
35899
+ allow: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35900
+ deny: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35411
35901
  });
35412
35902
  const warpOverride = {};
35413
35903
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -36119,6 +36609,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
36119
36609
  supportsImport: true
36120
36610
  }
36121
36611
  }],
36612
+ ["pi", {
36613
+ class: PiPermissions,
36614
+ meta: {
36615
+ supportsProject: true,
36616
+ supportsGlobal: true,
36617
+ supportsImport: true
36618
+ }
36619
+ }],
36122
36620
  ["qwencode", {
36123
36621
  class: QwencodePermissions,
36124
36622
  meta: {
@@ -38430,6 +38928,7 @@ const CursorSkillFrontmatterSchema = z.looseObject({
38430
38928
  description: z.string(),
38431
38929
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
38432
38930
  "disable-model-invocation": z.optional(z.boolean()),
38931
+ "user-invocable": z.optional(z.boolean()),
38433
38932
  metadata: z.optional(z.looseObject({}))
38434
38933
  });
38435
38934
  /**
@@ -38484,6 +38983,7 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38484
38983
  const cursorSection = {
38485
38984
  ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
38486
38985
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
38986
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
38487
38987
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
38488
38988
  };
38489
38989
  const rulesyncFrontmatter = {
@@ -38511,11 +39011,16 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38511
39011
  rootFrontmatter: rulesyncFrontmatter,
38512
39012
  section: cursorSection
38513
39013
  });
39014
+ const resolvedUserInvocable = resolveUserInvocable({
39015
+ rootFrontmatter: rulesyncFrontmatter,
39016
+ section: cursorSection
39017
+ });
38514
39018
  const cursorFrontmatter = {
38515
39019
  name: rulesyncFrontmatter.name,
38516
39020
  description: rulesyncFrontmatter.description,
38517
39021
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
38518
39022
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
39023
+ ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
38519
39024
  ...cursorSection?.metadata !== void 0 && { metadata: cursorSection.metadata }
38520
39025
  };
38521
39026
  return new CursorSkill({
@@ -46518,6 +47023,23 @@ var TaktSubagent = class TaktSubagent extends ToolSubagent {
46518
47023
  };
46519
47024
  //#endregion
46520
47025
  //#region src/features/subagents/vibe-subagent.ts
47026
+ /**
47027
+ * Where Vibe reads a custom system prompt from: `.vibe/prompts/<id>.md`
47028
+ * (project) and `~/.vibe/prompts/<id>.md` (user), with the custom directories
47029
+ * winning over the builtins. The id must be a bare file name with no path
47030
+ * separators.
47031
+ * @see vibe/core/prompts/__init__.py
47032
+ */
47033
+ const VIBE_PROMPTS_DIR_PATH = join(".vibe", "prompts");
47034
+ /**
47035
+ * Vibe resolves a `system_prompt_id` by file name, and
47036
+ * `VibeConfigSchema._check_system_prompt` evaluates it during validation — an
47037
+ * unresolvable id raises and the agent is dropped at discovery. The id must
47038
+ * therefore stay a bare slug with no path separators.
47039
+ */
47040
+ function promptSlug(relativeFilePath) {
47041
+ return basename(relativeFilePath).replace(/\.(md|toml)$/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
47042
+ }
46521
47043
  const VibeSubagentTomlSchema = z.looseObject({
46522
47044
  agent_type: z.enum(["agent", "subagent"]),
46523
47045
  display_name: z.optional(z.string()),
@@ -46534,14 +47056,22 @@ const VibeSubagentTomlSchema = z.looseObject({
46534
47056
  });
46535
47057
  var VibeSubagent = class VibeSubagent extends ToolSubagent {
46536
47058
  body;
46537
- constructor({ body, ...rest }) {
46538
- if (rest.validate !== false) try {
47059
+ promptFile;
47060
+ resolvedSystemPrompt;
47061
+ constructor({ body, promptFile = false, resolvedSystemPrompt, ...rest }) {
47062
+ if (rest.validate !== false && !promptFile) try {
46539
47063
  VibeSubagentTomlSchema.parse(smolToml.parse(body));
46540
47064
  } catch (error) {
46541
47065
  throw new Error(`Invalid TOML in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(error)}`, { cause: error });
46542
47066
  }
46543
47067
  super({ ...rest });
46544
47068
  this.body = body;
47069
+ this.promptFile = promptFile;
47070
+ this.resolvedSystemPrompt = resolvedSystemPrompt;
47071
+ }
47072
+ /** True for the companion `.vibe/prompts/<id>.md` file. */
47073
+ isPromptFile() {
47074
+ return this.promptFile;
46545
47075
  }
46546
47076
  static getSettablePaths(_options = {}) {
46547
47077
  return { relativeDirPath: join(".vibe", "agents") };
@@ -46556,7 +47086,9 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46556
47086
  } catch (error) {
46557
47087
  throw new Error(`Failed to parse TOML in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
46558
47088
  }
46559
- const { system_prompt, description, display_name, ...vibeSection } = parsed;
47089
+ const { system_prompt, system_prompt_id, description, display_name, ...vibeSection } = parsed;
47090
+ const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
47091
+ const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
46560
47092
  const fileStem = basename(this.getRelativeFilePath(), ".toml");
46561
47093
  const rulesyncFrontmatter = {
46562
47094
  targets: ["vibe"],
@@ -46565,13 +47097,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46565
47097
  vibe: {
46566
47098
  ...display_name !== void 0 && { display_name },
46567
47099
  ...description !== void 0 && { description },
47100
+ ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
46568
47101
  ...vibeSection
46569
47102
  }
46570
47103
  };
46571
47104
  return new RulesyncSubagent({
46572
47105
  outputRoot: this.outputRoot,
46573
47106
  frontmatter: rulesyncFrontmatter,
46574
- body: system_prompt ?? "",
47107
+ body: resolvedBody ?? "",
46575
47108
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
46576
47109
  relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
46577
47110
  validate: true
@@ -46586,12 +47119,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46586
47119
  "description",
46587
47120
  "system_prompt"
46588
47121
  ]);
47122
+ const promptBody = rulesyncSubagent.getBody();
47123
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
46589
47124
  const tomlObj = {
46590
47125
  agent_type: rawSection.agent_type === "agent" ? "agent" : "subagent",
46591
47126
  display_name: typeof rawSection.display_name === "string" ? rawSection.display_name : frontmatter.name,
46592
47127
  ...frontmatter.description ? { description: frontmatter.description } : {},
46593
- ...rulesyncSubagent.getBody() ? { system_prompt: rulesyncSubagent.getBody() } : {},
46594
- ...vibeSection
47128
+ ...vibeSection,
47129
+ ...promptBody ? { system_prompt_id: slug } : {}
46595
47130
  };
46596
47131
  const body = smolToml.stringify(tomlObj);
46597
47132
  const paths = this.getSettablePaths({ global });
@@ -46606,6 +47141,36 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46606
47141
  global
46607
47142
  });
46608
47143
  }
47144
+ /**
47145
+ * Emits the agent TOML plus its companion prompt file. The prompt file MUST
47146
+ * be written together with the id: `VibeConfigSchema._check_system_prompt`
47147
+ * evaluates the property during validation, so an unresolvable
47148
+ * `system_prompt_id` raises and `AgentRegistry._try_load` drops the agent
47149
+ * with a warning.
47150
+ */
47151
+ static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false }) {
47152
+ return rulesyncSubagents.flatMap((rulesyncSubagent) => {
47153
+ const agentFile = VibeSubagent.fromRulesyncSubagent({
47154
+ outputRoot,
47155
+ relativeDirPath: VibeSubagent.getSettablePaths({ global }).relativeDirPath,
47156
+ rulesyncSubagent,
47157
+ global
47158
+ });
47159
+ const promptBody = rulesyncSubagent.getBody();
47160
+ if (!promptBody) return [agentFile];
47161
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
47162
+ return [agentFile, new VibeSubagent({
47163
+ outputRoot,
47164
+ body: promptBody,
47165
+ relativeDirPath: VIBE_PROMPTS_DIR_PATH,
47166
+ relativeFilePath: `${slug}.md`,
47167
+ fileContent: promptBody,
47168
+ promptFile: true,
47169
+ validate: false,
47170
+ global
47171
+ })];
47172
+ });
47173
+ }
46609
47174
  validate() {
46610
47175
  try {
46611
47176
  VibeSubagentTomlSchema.parse(smolToml.parse(this.body));
@@ -46630,6 +47195,10 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46630
47195
  const paths = this.getSettablePaths({ global });
46631
47196
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
46632
47197
  const fileContent = await readFileContent(filePath);
47198
+ const resolvedSystemPrompt = await VibeSubagent.readSystemPrompt({
47199
+ outputRoot,
47200
+ fileContent
47201
+ });
46633
47202
  const subagent = new VibeSubagent({
46634
47203
  outputRoot,
46635
47204
  relativeDirPath: paths.relativeDirPath,
@@ -46637,7 +47206,8 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46637
47206
  body: fileContent.trim(),
46638
47207
  fileContent,
46639
47208
  validate,
46640
- global
47209
+ global,
47210
+ ...resolvedSystemPrompt !== void 0 && { resolvedSystemPrompt }
46641
47211
  });
46642
47212
  if (validate) {
46643
47213
  const result = subagent.validate();
@@ -46645,6 +47215,21 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46645
47215
  }
46646
47216
  return subagent;
46647
47217
  }
47218
+ /**
47219
+ * Read `.vibe/prompts/<id>.md` for an agent that names a `system_prompt_id`.
47220
+ * Returns `undefined` when there is no id or the file is missing, in which
47221
+ * case import falls back to the legacy `system_prompt` key.
47222
+ */
47223
+ static async readSystemPrompt({ outputRoot, fileContent }) {
47224
+ let promptId;
47225
+ try {
47226
+ promptId = VibeSubagentTomlSchema.parse(smolToml.parse(fileContent)).system_prompt_id;
47227
+ } catch {
47228
+ return;
47229
+ }
47230
+ if (typeof promptId !== "string" || promptId === "" || /[\\/]/.test(promptId)) return;
47231
+ return await readFileContentOrNull(join(outputRoot, VIBE_PROMPTS_DIR_PATH, `${promptId}.md`)) ?? void 0;
47232
+ }
46648
47233
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46649
47234
  return new VibeSubagent({
46650
47235
  outputRoot,
@@ -46687,7 +47272,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46687
47272
  const mode = super.toRooMode(rulesyncSubagent);
46688
47273
  const frontmatter = rulesyncSubagent.getFrontmatter();
46689
47274
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
46690
- if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
47275
+ if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
46691
47276
  return mode;
46692
47277
  }
46693
47278
  toRulesyncSubagents() {
@@ -46699,7 +47284,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46699
47284
  targets: ["zoocode"],
46700
47285
  roo: restRooSection
46701
47286
  };
46702
- if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
47287
+ if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
46703
47288
  return new RulesyncSubagent({
46704
47289
  outputRoot: ".",
46705
47290
  frontmatter: rebuilt,
@@ -49602,8 +50187,13 @@ const STRATEGIES = [
49602
50187
  * file per rule under `.devin/rules/*.md` (Devin Desktop Cascade) with YAML
49603
50188
  * frontmatter carrying a `trigger` (always_on | glob | manual | model_decision)
49604
50189
  * 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).
50190
+ * - Global scope: mirrors the project layout. The root rule is a single
50191
+ * plain-markdown, always-on file (no frontmatter) at
50192
+ * `~/.config/devin/AGENTS.md`; non-root rules are one file per rule under
50193
+ * `~/.devin/rules/*.md` with the same `trigger`/`globs` frontmatter as project
50194
+ * scope. Note the directory split — the per-rule global directory is the home
50195
+ * `~/.devin/`, not the `~/.config/devin/` tree every other Devin global
50196
+ * surface uses. That is what the rules page documents.
49607
50197
  *
49608
50198
  * Trigger inference (when no explicit devin trigger is persisted):
49609
50199
  * - Specific globs (non wildcard) → glob
@@ -49634,19 +50224,32 @@ var DevinRule = class DevinRule extends ToolRule {
49634
50224
  relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
49635
50225
  };
49636
50226
  }
50227
+ /**
50228
+ * The always-on root rule file for a scope: the project-root `AGENTS.md`
50229
+ * Devin CLI / Devin Local reads, or `~/.config/devin/AGENTS.md` in global
50230
+ * scope. Single-sourced so `getSettablePaths`, `fromFile` and
50231
+ * `fromRulesyncRule` cannot disagree about it.
50232
+ */
50233
+ static getRootPath(global, excludeToolDir) {
50234
+ return global ? DevinRule.getGlobalRootPath(excludeToolDir) : {
50235
+ relativeDirPath: ".",
50236
+ relativeFilePath: "AGENTS.md"
50237
+ };
50238
+ }
49637
50239
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
49638
- if (global) return { root: DevinRule.getGlobalRootPath(excludeToolDir) };
50240
+ if (global) return {
50241
+ root: DevinRule.getRootPath(true, excludeToolDir),
50242
+ nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
50243
+ };
49639
50244
  return {
49640
- root: {
49641
- relativeDirPath: ".",
49642
- relativeFilePath: "AGENTS.md"
49643
- },
50245
+ root: DevinRule.getRootPath(false, excludeToolDir),
49644
50246
  nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
49645
50247
  };
49646
50248
  }
49647
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
49648
- if (global) {
49649
- const rootPath = DevinRule.getGlobalRootPath();
50249
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
50250
+ const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
50251
+ if (relativeDirPath !== void 0 ? relativeDirPath !== nonRootDirPath : relativeFilePath === DevinRule.getRootPath(global).relativeFilePath) {
50252
+ const rootPath = DevinRule.getRootPath(global);
49650
50253
  const fileContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
49651
50254
  return new DevinRule({
49652
50255
  outputRoot,
@@ -49658,19 +50261,6 @@ var DevinRule = class DevinRule extends ToolRule {
49658
50261
  root: true
49659
50262
  });
49660
50263
  }
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
50264
  const filePath = join(outputRoot, nonRootDirPath, relativeFilePath);
49675
50265
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
49676
50266
  let parsedFrontmatter;
@@ -49690,8 +50280,9 @@ var DevinRule = class DevinRule extends ToolRule {
49690
50280
  });
49691
50281
  }
49692
50282
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
49693
- if (global) {
49694
- const rootPath = DevinRule.getGlobalRootPath();
50283
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
50284
+ if (rulesyncFrontmatter.root) {
50285
+ const rootPath = DevinRule.getRootPath(global);
49695
50286
  return new DevinRule({
49696
50287
  outputRoot,
49697
50288
  relativeDirPath: rootPath.relativeDirPath,
@@ -49702,16 +50293,6 @@ var DevinRule = class DevinRule extends ToolRule {
49702
50293
  root: true
49703
50294
  });
49704
50295
  }
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
50296
  const storedDevin = rulesyncFrontmatter.devin;
49716
50297
  const normalized = normalizeStoredDevin(storedDevin);
49717
50298
  const storedTrigger = storedDevin?.trigger;
@@ -49771,7 +50352,7 @@ var DevinRule = class DevinRule extends ToolRule {
49771
50352
  error: null
49772
50353
  };
49773
50354
  }
49774
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50355
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
49775
50356
  return new DevinRule({
49776
50357
  outputRoot,
49777
50358
  relativeDirPath,
@@ -49779,7 +50360,7 @@ var DevinRule = class DevinRule extends ToolRule {
49779
50360
  frontmatter: {},
49780
50361
  body: "",
49781
50362
  validate: false,
49782
- root: global
50363
+ root: relativeDirPath !== buildToolPath(DEVIN_DIR, "rules")
49783
50364
  });
49784
50365
  }
49785
50366
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -50412,6 +50993,11 @@ var KimiCodeRule = class KimiCodeRule extends ToolRule {
50412
50993
  };
50413
50994
  //#endregion
50414
50995
  //#region src/features/rules/kiro-rule.ts
50996
+ /**
50997
+ * Kiro loads `AGENTS.md` as steering context from any directory in the
50998
+ * workspace tree (CLI 2.18.0 / IDE 1.0.309), not just the workspace root.
50999
+ */
51000
+ const KIRO_NESTED_STEERING_FILE_NAME = "AGENTS.md";
50415
51001
  const WILDCARD_GLOBS = /* @__PURE__ */ new Set([
50416
51002
  "**/*",
50417
51003
  "**",
@@ -50485,8 +51071,17 @@ var KiroRule = class KiroRule extends ToolRule {
50485
51071
  };
50486
51072
  return { nonRoot: { relativeDirPath: steeringDirPath } };
50487
51073
  }
50488
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
51074
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
50489
51075
  const paths = this.getSettablePaths({ global });
51076
+ const steeringDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
51077
+ if (overrideDirPath !== void 0 && overrideDirPath !== steeringDirPath && overrideDirPath !== "." && relativeFilePath === KIRO_NESTED_STEERING_FILE_NAME) return new KiroRule({
51078
+ outputRoot,
51079
+ relativeDirPath: overrideDirPath,
51080
+ relativeFilePath,
51081
+ fileContent: await readFileContent(join(outputRoot, overrideDirPath, relativeFilePath)),
51082
+ validate,
51083
+ root: false
51084
+ });
50490
51085
  const isRoot = "root" in paths && relativeFilePath === paths.root.relativeFilePath;
50491
51086
  const relativeDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
50492
51087
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
@@ -50510,6 +51105,12 @@ var KiroRule = class KiroRule extends ToolRule {
50510
51105
  });
50511
51106
  if (params.root) return new KiroRule(params);
50512
51107
  const frontmatter = rulesyncRule.getFrontmatter();
51108
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
51109
+ if (!global && subprojectPath) return new KiroRule({
51110
+ ...params,
51111
+ relativeDirPath: join(subprojectPath),
51112
+ relativeFilePath: KIRO_NESTED_STEERING_FILE_NAME
51113
+ });
50513
51114
  const inclusion = deriveKiroInclusion({
50514
51115
  kiro: frontmatter.kiro,
50515
51116
  globs: frontmatter.globs
@@ -50520,7 +51121,34 @@ var KiroRule = class KiroRule extends ToolRule {
50520
51121
  fileContent: stringifyFrontmatter(rulesyncRule.getBody(), inclusion)
50521
51122
  });
50522
51123
  }
51124
+ /**
51125
+ * The subproject directory a nested `AGENTS.md` scopes, or `undefined` for the
51126
+ * root rule and for the `.kiro/steering/` files.
51127
+ */
51128
+ getSubprojectPath() {
51129
+ if (this.isRoot() || this.getRelativeFilePath() !== KIRO_NESTED_STEERING_FILE_NAME) return;
51130
+ const relativeDirPath = toPosixPath(this.getRelativeDirPath());
51131
+ if (relativeDirPath === "." || relativeDirPath === "" || relativeDirPath.startsWith(".")) return;
51132
+ return relativeDirPath;
51133
+ }
50523
51134
  toRulesyncRule() {
51135
+ const subprojectPath = this.getSubprojectPath();
51136
+ if (subprojectPath !== void 0) {
51137
+ const slug = subprojectPath.replaceAll("/", "-");
51138
+ return new RulesyncRule({
51139
+ outputRoot: this.getOutputRoot(),
51140
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
51141
+ relativeFilePath: `${slug}-kiro.md`,
51142
+ frontmatter: {
51143
+ targets: ["kiro"],
51144
+ root: false,
51145
+ globs: [`${subprojectPath}/**/*`],
51146
+ agentsmd: { subprojectPath }
51147
+ },
51148
+ body: this.getFileContent(),
51149
+ validate: true
51150
+ });
51151
+ }
50524
51152
  const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getRelativeFilePath());
50525
51153
  const inclusion = typeof frontmatter.inclusion === "string" ? frontmatter.inclusion : void 0;
50526
51154
  if (!inclusion) return this.toRulesyncRuleDefault();
@@ -50553,6 +51181,26 @@ var KiroRule = class KiroRule extends ToolRule {
50553
51181
  error: null
50554
51182
  };
50555
51183
  }
51184
+ /**
51185
+ * Nested `AGENTS.md` files anywhere in the workspace tree, which Kiro loads as
51186
+ * steering context alongside `.kiro/steering/`. Import-only, like every other
51187
+ * nested scan: these are hand-authored files outside a rulesync-owned
51188
+ * directory, so enumerating them for `--delete` would sweep away work rulesync
51189
+ * never wrote.
51190
+ * @see https://kiro.dev/docs/steering/
51191
+ */
51192
+ static getNestedFilePatterns({ outputRoot }) {
51193
+ const root = toPosixPath(outputRoot);
51194
+ return {
51195
+ include: [`${root}/**/${KIRO_NESTED_STEERING_FILE_NAME}`],
51196
+ ignore: [
51197
+ `${root}/${KIRO_NESTED_STEERING_FILE_NAME}`,
51198
+ `${root}/**/.*/**`,
51199
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
51200
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
51201
+ ]
51202
+ };
51203
+ }
50556
51204
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50557
51205
  return new KiroRule({
50558
51206
  outputRoot,
@@ -52021,6 +52669,18 @@ const resolveIncludeLocalRoot = (options) => {
52021
52669
  return parsed.data.includeLocalRoot ?? true;
52022
52670
  };
52023
52671
  /**
52672
+ * Whether `candidate` is `base` itself or a class that extends it.
52673
+ *
52674
+ * Class-identity dispatch (`factory.class === Base`) is subclass-blind: a target
52675
+ * whose adapter merely narrows another one — `ZoocodeRule extends RooRule` — fails
52676
+ * a strict `===` and silently falls through. That is how `zoocode` ended up
52677
+ * declaring `localRootMode: "separate-local-file"` while never emitting
52678
+ * `AGENTS.local.md`, in both directions and without a warning.
52679
+ */
52680
+ function isClassOrSubclassOf({ candidate, base }) {
52681
+ return candidate === base || Object.prototype.isPrototypeOf.call(base, candidate);
52682
+ }
52683
+ /**
52024
52684
  * Factory Map mapping tool targets to their rule factories.
52025
52685
  * Using Map to preserve insertion order for consistent iteration.
52026
52686
  */
@@ -52374,7 +53034,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
52374
53034
  meta: {
52375
53035
  extension: "md",
52376
53036
  supportsGlobal: true,
52377
- ruleDiscoveryMode: "auto"
53037
+ ruleDiscoveryMode: "auto",
53038
+ localRootMode: "separate-local-file",
53039
+ localRootFileName: "AGENTS.local.md"
52378
53040
  }
52379
53041
  }],
52380
53042
  ["zed", {
@@ -52661,7 +53323,10 @@ var RulesProcessor = class extends FeatureProcessor {
52661
53323
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
52662
53324
  }
52663
53325
  buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
52664
- if (factory.class === ClaudecodeRule) {
53326
+ if (isClassOrSubclassOf({
53327
+ candidate: factory.class,
53328
+ base: ClaudecodeRule
53329
+ })) {
52665
53330
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
52666
53331
  return new ClaudecodeRule({
52667
53332
  outputRoot: this.outputRoot,
@@ -52674,7 +53339,10 @@ var RulesProcessor = class extends FeatureProcessor {
52674
53339
  localRoot
52675
53340
  });
52676
53341
  }
52677
- if (factory.class === ClaudecodeLegacyRule) {
53342
+ if (isClassOrSubclassOf({
53343
+ candidate: factory.class,
53344
+ base: ClaudecodeLegacyRule
53345
+ })) {
52678
53346
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
52679
53347
  return new ClaudecodeLegacyRule({
52680
53348
  outputRoot: this.outputRoot,
@@ -52686,7 +53354,10 @@ var RulesProcessor = class extends FeatureProcessor {
52686
53354
  localRoot
52687
53355
  });
52688
53356
  }
52689
- if (factory.class === RovodevRule) return new RovodevRule({
53357
+ if (isClassOrSubclassOf({
53358
+ candidate: factory.class,
53359
+ base: RovodevRule
53360
+ })) return new RovodevRule({
52690
53361
  outputRoot: this.outputRoot,
52691
53362
  relativeDirPath: relativeDirPath ?? ".",
52692
53363
  relativeFilePath: fileName,
@@ -52695,7 +53366,10 @@ var RulesProcessor = class extends FeatureProcessor {
52695
53366
  root: true,
52696
53367
  localRoot
52697
53368
  });
52698
- if (factory.class === RooRule) return new RooRule({
53369
+ if (isClassOrSubclassOf({
53370
+ candidate: factory.class,
53371
+ base: RooRule
53372
+ })) return new RooRule({
52699
53373
  outputRoot: this.outputRoot,
52700
53374
  relativeDirPath: relativeDirPath ?? ".",
52701
53375
  relativeFilePath: fileName,
@@ -52704,7 +53378,23 @@ var RulesProcessor = class extends FeatureProcessor {
52704
53378
  root: true,
52705
53379
  localRoot
52706
53380
  });
52707
- if (factory.class === QwencodeRule) return new QwencodeRule({
53381
+ if (isClassOrSubclassOf({
53382
+ candidate: factory.class,
53383
+ base: DevinRule
53384
+ })) return new DevinRule({
53385
+ outputRoot: this.outputRoot,
53386
+ relativeDirPath: relativeDirPath ?? ".",
53387
+ relativeFilePath: fileName,
53388
+ frontmatter: {},
53389
+ body,
53390
+ validate: true,
53391
+ root: true,
53392
+ localRoot
53393
+ });
53394
+ if (isClassOrSubclassOf({
53395
+ candidate: factory.class,
53396
+ base: QwencodeRule
53397
+ })) return new QwencodeRule({
52708
53398
  outputRoot: this.outputRoot,
52709
53399
  relativeDirPath: relativeDirPath ?? ".qwen",
52710
53400
  relativeFilePath: fileName,
@@ -54921,4 +55611,4 @@ async function importChecksCore(params) {
54921
55611
  //#endregion
54922
55612
  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
55613
 
54924
- //# sourceMappingURL=import-zGCKgpdt.js.map
55614
+ //# sourceMappingURL=import-Dswkc7Ub.js.map