rulesync 16.12.0 → 16.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -134,27 +134,27 @@ const isFeatureValueEnabled = (value) => {
134
134
  };
135
135
  //#endregion
136
136
  //#region src/constants/rulesync-paths.ts
137
- const { join: join$288 } = node_path.posix;
137
+ const { join: join$290 } = node_path.posix;
138
138
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
139
139
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
140
140
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
141
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
- join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
141
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$290(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
+ join$290(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
152
152
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
153
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
153
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
154
154
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
155
155
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
156
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
156
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$290(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
158
158
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
159
159
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
160
160
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -437,13 +437,15 @@ const permissionsProcessorToolTargetTuple = [
437
437
  "kiro-cli",
438
438
  "kiro-ide",
439
439
  "opencode",
440
+ "pi",
440
441
  "qwencode",
441
442
  "reasonix",
442
443
  "rovodev",
443
444
  "takt",
444
445
  "vibe",
445
446
  "warp",
446
- "zed"
447
+ "zed",
448
+ "zoocode"
447
449
  ];
448
450
  const checksProcessorToolTargetTuple = [
449
451
  "amp",
@@ -1928,7 +1930,7 @@ function isPlainObject$1(value) {
1928
1930
  /**
1929
1931
  * Type guard to check if a value is an array of strings.
1930
1932
  */
1931
- function isStringArray$1(value) {
1933
+ function isStringArray$2(value) {
1932
1934
  return Array.isArray(value) && value.every((item) => typeof item === "string");
1933
1935
  }
1934
1936
  //#endregion
@@ -2296,6 +2298,7 @@ const HOOK_EVENTS = [
2296
2298
  "todoCreated",
2297
2299
  "todoCompleted",
2298
2300
  "stopFailure",
2301
+ "stopCancelled",
2299
2302
  "instructionsLoaded",
2300
2303
  "userPromptExpansion",
2301
2304
  "postToolBatch",
@@ -2871,6 +2874,7 @@ const GROKCLI_HOOK_EVENTS = [
2871
2874
  "permissionDenied",
2872
2875
  "stop",
2873
2876
  "stopFailure",
2877
+ "stopCancelled",
2874
2878
  "notification",
2875
2879
  "subagentStart",
2876
2880
  "subagentStop",
@@ -2946,16 +2950,10 @@ const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CAN
2946
2950
  /**
2947
2951
  * Hook events supported by Hermes Agent's native Shell Hooks system.
2948
2952
  *
2949
- * Hermes validates hook events against a fixed `VALID_HOOKS` set:
2950
- * `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
2951
- * `pre_verify`, `pre_api_request`, `post_api_request`, `api_request_error`,
2952
- * `on_session_start`, `on_session_end`, `on_session_finalize`,
2953
- * `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
2954
- * `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
2955
- * `transform_terminal_output`, `transform_llm_output`, and the three
2956
- * `kanban_task_*` events. Only the events with a clean 1:1 canonical equivalent
2957
- * are mapped here. All other native events round-trip through
2958
- * `hermesagent.hooks`.
2953
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
2954
+ * of v0.20.2 (`v2026.8.16`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
2955
+ * full list. Only the events with a clean 1:1 canonical equivalent are mapped
2956
+ * here. All other native events round-trip through `hermesagent.hooks`.
2959
2957
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
2960
2958
  */
2961
2959
  const HERMESAGENT_HOOK_EVENTS = [
@@ -2968,6 +2966,21 @@ const HERMESAGENT_HOOK_EVENTS = [
2968
2966
  "subagentStart",
2969
2967
  "subagentStop"
2970
2968
  ];
2969
+ /**
2970
+ * Hermes Agent's native `VALID_HOOKS` set, in upstream source order.
2971
+ *
2972
+ * The authority is `hermes_cli/plugins.py`'s `VALID_HOOKS`, not the docs: at
2973
+ * v0.20.2 `website/docs/user-guide/features/plugins.md` says "26 lifecycle
2974
+ * events" and then lists 30, omitting the five `on_kanban_*` worker/task/dispatch
2975
+ * observers, `gateway_platform_event` and `transform_api_error_classification`.
2976
+ * Shell hooks are gated on the same set.
2977
+ *
2978
+ * Verified against the tag `v2026.8.16` (v0.20.2): `VALID_HOOKS` holds 37
2979
+ * entries, grown from 23 at `v2026.8.3` (v0.20.0). This list holds 36 of them —
2980
+ * see {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
2981
+ *
2982
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py
2983
+ */
2971
2984
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2972
2985
  "pre_tool_call",
2973
2986
  "post_tool_call",
@@ -2976,6 +2989,10 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2976
2989
  "transform_llm_output",
2977
2990
  "pre_llm_call",
2978
2991
  "post_llm_call",
2992
+ "on_stream_start",
2993
+ "on_stream_delta",
2994
+ "on_stream_end",
2995
+ "on_interim_message",
2979
2996
  "pre_verify",
2980
2997
  "pre_api_request",
2981
2998
  "post_api_request",
@@ -2984,16 +3001,39 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2984
3001
  "on_session_end",
2985
3002
  "on_session_finalize",
2986
3003
  "on_session_reset",
3004
+ "on_skill_lifecycle",
2987
3005
  "subagent_start",
2988
3006
  "subagent_stop",
2989
3007
  "pre_gateway_dispatch",
2990
3008
  "pre_approval_request",
2991
3009
  "post_approval_response",
3010
+ "pre_transcription",
2992
3011
  "kanban_task_claimed",
2993
3012
  "kanban_task_completed",
2994
- "kanban_task_blocked"
3013
+ "kanban_task_blocked",
3014
+ "on_kanban_worker_spawned",
3015
+ "on_kanban_worker_exited",
3016
+ "on_kanban_worker_stale_claim",
3017
+ "on_kanban_task_updated",
3018
+ "on_kanban_dispatch_tick",
3019
+ "gateway_platform_event",
3020
+ "pre_command"
2995
3021
  ];
2996
3022
  /**
3023
+ * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
3024
+ * as the shell-hook allow-list, but `SHELL_UNSUPPORTED_HOOKS` is subtracted from
3025
+ * it first: shell hooks cannot return these events' directives, so
3026
+ * `_parse_hooks_block` refuses the registration with a warning rather than
3027
+ * accepting it and dropping the return value.
3028
+ *
3029
+ * rulesync only ever writes shell hooks, so these are excluded from
3030
+ * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
3031
+ *
3032
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
3033
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/agent/shell_hooks.py — `_parse_hooks_block`
3034
+ */
3035
+ const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
3036
+ /**
2997
3037
  * Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
2998
3038
  * snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
2999
3039
  */
@@ -3610,6 +3650,7 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
3610
3650
  permissionDenied: "PermissionDenied",
3611
3651
  stop: "Stop",
3612
3652
  stopFailure: "StopFailure",
3653
+ stopCancelled: "StopCancelled",
3613
3654
  notification: "Notification",
3614
3655
  subagentStart: "SubagentStart",
3615
3656
  subagentStop: "SubagentStop",
@@ -3884,6 +3925,25 @@ const McpServerSchema = zod_mini.z.looseObject({
3884
3925
  kiroAutoApprove: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
3885
3926
  kiroAutoBlock: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
3886
3927
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
3928
+ /**
3929
+ * The canonical per-server tool allowlist.
3930
+ *
3931
+ * **Collision rule for adapters.** Several tools spell an allowlist natively
3932
+ * under a name that also exists here, so a single server entry can carry both
3933
+ * a canonical field and its native counterpart. What the *native* key means
3934
+ * decides the resolution — not which one was written first:
3935
+ *
3936
+ * - Native key is a redundant spelling of the same concept, and both lists are
3937
+ * additive → **merge** them, so a config that already spells the field
3938
+ * natively keeps working (`kiroAutoApprove`/`kiroAutoBlock` in
3939
+ * `kiro-mcp.ts`).
3940
+ * - Native key is the same concept but rulesync owns the generated value, so
3941
+ * keeping both would be ambiguous → the **canonical value wins** and the
3942
+ * native one is dropped with a warning (`tools` in `copilotcli-mcp.ts`).
3943
+ * - Native key of the same name means something else entirely → **refuse the
3944
+ * canonical value** rather than write a shape the tool misreads (`tools` in
3945
+ * `codexcli-mcp.ts`, where Codex reads it as a per-tool approval table).
3946
+ */
3887
3947
  enabledTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
3888
3948
  disabledTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
3889
3949
  });
@@ -4726,8 +4786,65 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4726
4786
  dangerouslyAllowAll: zod_mini.z.optional(zod_mini.z.boolean())
4727
4787
  });
4728
4788
  /**
4789
+ * Tool-scoped override block for Pi Coding Agent. Pi has no allow/ask/deny rule
4790
+ * surface: the only repository-syncable tool gate is `defaultTools`, the list of
4791
+ * built-in tools enabled at startup (added in v0.84.2). It is an enable-list
4792
+ * rather than a canonical allow/deny rule set, so it is authored here rather
4793
+ * than mapped onto `permission`, mirroring `KimiCodePermissionsOverrideSchema`'s
4794
+ * `tools.enabled` and `VibePermissionsOverrideSchema`'s `enabled_tools`.
4795
+ *
4796
+ * An empty array is meaningful upstream — it starts Pi with no built-in tools
4797
+ * while keeping extension and SDK custom tools — so it is emitted as written.
4798
+ * CLI flags (`--tools`, `--no-tools`, `--no-builtin-tools`, `--exclude-tools`)
4799
+ * outrank the setting, and a project array **replaces** the global one rather
4800
+ * than merging with it.
4801
+ *
4802
+ * @example
4803
+ * { "defaultTools": ["bash", "edit", "write"] }
4804
+ * @see https://pi.dev/docs/latest/settings
4805
+ */
4806
+ const PiPermissionsOverrideSchema = zod_mini.z.looseObject({
4807
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4808
+ defaultTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
4809
+ });
4810
+ /**
4811
+ * The Antigravity CLI's four documented `toolPermission` autonomy presets.
4812
+ * Exported so the import side of `antigravity-cli-permissions.ts` filters
4813
+ * against exactly the values the enum below accepts — an unrecognized value
4814
+ * carried in verbatim would fail validation of the *whole*
4815
+ * `.rulesync/permissions.jsonc`, not just that key.
4816
+ * @see https://antigravity.google/docs/cli/reference
4817
+ */
4818
+ const ANTIGRAVITY_CLI_TOOL_PERMISSIONS = [
4819
+ "request-review",
4820
+ "proceed-in-sandbox",
4821
+ "always-proceed",
4822
+ "strict"
4823
+ ];
4824
+ /**
4825
+ * The Antigravity CLI's three documented `artifactReviewPolicy` values. Shared
4826
+ * with the import filter for the same reason as
4827
+ * {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4828
+ * @see https://antigravity.google/docs/cli/settings
4829
+ */
4830
+ const ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES = [
4831
+ "asks-for-review",
4832
+ "agent-decides",
4833
+ "always-proceed"
4834
+ ];
4835
+ /**
4836
+ * The baseline execution modes the CLI persists as `agentMode`. Shared with the
4837
+ * import filter for the same reason as {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4838
+ * @see https://antigravity.google/docs/cli/modes
4839
+ */
4840
+ const ANTIGRAVITY_CLI_AGENT_MODES = [
4841
+ "default",
4842
+ "accept-edits",
4843
+ "plan"
4844
+ ];
4845
+ /**
4729
4846
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4730
- * `settings.json` carries four global autonomy/sandbox knobs outside the
4847
+ * `settings.json` carries five global autonomy/sandbox knobs outside the
4731
4848
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4732
4849
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4733
4850
  * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
@@ -4735,7 +4852,8 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4735
4852
  * agent's artifact changes are gated on a review prompt — `asks-for-review`
4736
4853
  * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4737
4854
  * (a boolean, off by default, letting the agent read or write files outside the
4738
- * active workspace roots). Antigravity applies the allow/deny
4855
+ * active workspace roots), and `agentMode` (the baseline execution mode a session
4856
+ * starts in — `default` / `accept-edits` / `plan`). Antigravity applies the allow/deny
4739
4857
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4740
4858
  * authors these keys verbatim — no precedence modeling is needed on our side.
4741
4859
  * Fields placed here are merged onto the top level of
@@ -4743,27 +4861,27 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4743
4861
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4744
4862
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4745
4863
  * Verified against https://antigravity.google/docs/cli/reference,
4746
- * https://antigravity.google/docs/cli/sandbox and
4747
- * https://antigravity.google/docs/cli/settings.
4864
+ * https://antigravity.google/docs/cli/sandbox,
4865
+ * https://antigravity.google/docs/cli/settings and
4866
+ * https://antigravity.google/docs/cli/modes.
4748
4867
  *
4749
4868
  * @example
4750
4869
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
4751
4870
  */
4752
4871
  const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
4753
4872
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4754
- toolPermission: zod_mini.z.optional(zod_mini.z.enum([
4755
- "request-review",
4756
- "proceed-in-sandbox",
4757
- "always-proceed",
4758
- "strict"
4759
- ])),
4873
+ toolPermission: zod_mini.z.optional(zod_mini.z.enum(ANTIGRAVITY_CLI_TOOL_PERMISSIONS)),
4760
4874
  enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean()),
4761
- artifactReviewPolicy: zod_mini.z.optional(zod_mini.z.enum([
4762
- "asks-for-review",
4763
- "agent-decides",
4764
- "always-proceed"
4765
- ])),
4766
- allowNonWorkspaceAccess: zod_mini.z.optional(zod_mini.z.boolean())
4875
+ artifactReviewPolicy: zod_mini.z.optional(zod_mini.z.enum(ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES)),
4876
+ allowNonWorkspaceAccess: zod_mini.z.optional(zod_mini.z.boolean()),
4877
+ /**
4878
+ * The baseline execution mode the CLI starts a session in (Agent Mode), the
4879
+ * persisted form of the `/settings` panel's Agent Mode row. Only
4880
+ * `~/.gemini/antigravity-cli/settings.json` is read for it, which is why this
4881
+ * override stays global-only.
4882
+ * @see https://antigravity.google/docs/cli/modes
4883
+ */
4884
+ agentMode: zod_mini.z.optional(zod_mini.z.enum(ANTIGRAVITY_CLI_AGENT_MODES))
4767
4885
  });
4768
4886
  /**
4769
4887
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -5073,6 +5191,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
5073
5191
  takt: zod_mini.z.optional(TaktPermissionsOverrideSchema),
5074
5192
  amp: zod_mini.z.optional(AmpPermissionsOverrideSchema),
5075
5193
  "antigravity-cli": zod_mini.z.optional(AntigravityCliPermissionsOverrideSchema),
5194
+ pi: zod_mini.z.optional(PiPermissionsOverrideSchema),
5076
5195
  augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
5077
5196
  kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
5078
5197
  codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
@@ -5252,6 +5371,7 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
5252
5371
  systemPrompt: zod_mini.z.optional(zod_mini.z.enum(["append"])),
5253
5372
  contextFile: zod_mini.z.optional(zod_mini.z.enum(["override"]))
5254
5373
  })),
5374
+ roo: zod_mini.z.optional(zod_mini.z.looseObject({ mode: zod_mini.z.optional(zod_mini.z.string()) })),
5255
5375
  takt: zod_mini.z.optional(zod_mini.z.looseObject({
5256
5376
  name: zod_mini.z.optional(zod_mini.z.string()),
5257
5377
  extends: zod_mini.z.optional(zod_mini.z.string()),
@@ -5554,6 +5674,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
5554
5674
  cursor: zod_mini.z.optional(zod_mini.z.looseObject({
5555
5675
  paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
5556
5676
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
5677
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
5557
5678
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
5558
5679
  })),
5559
5680
  factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -5778,6 +5899,11 @@ async function getLocalSkillDirNames(outputRoot) {
5778
5899
  * default with a per-target value. A defined section value (including `false`)
5779
5900
  * always wins over the root default.
5780
5901
  *
5902
+ * `devin` also consumes the root-level value — it maps `true` onto a user-only
5903
+ * `triggers` list (see `devin-skill.ts`) — but has no section key of the same
5904
+ * name, so it does not go through this helper. A `devin.triggers` section value
5905
+ * still overrides it.
5906
+ *
5781
5907
  * @returns The resolved boolean, or `undefined` when neither value is set.
5782
5908
  */
5783
5909
  function resolveDisableModelInvocation({ rootFrontmatter, section }) {
@@ -5788,10 +5914,15 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
5788
5914
  *
5789
5915
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
5790
5916
  * that applies to every tool supporting the flag (claudecode, copilot,
5791
- * copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5917
+ * copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5792
5918
  * per-target value. A defined section value (including `false`) always wins
5793
5919
  * over the root default.
5794
5920
  *
5921
+ * `devin` also consumes the root-level value — it maps `false` onto a
5922
+ * model-only `triggers` list (see `devin-skill.ts`) — but has no section key of
5923
+ * the same name, so it does not go through this helper. A `devin.triggers`
5924
+ * section value still overrides it.
5925
+ *
5795
5926
  * @returns The resolved boolean, or `undefined` when neither value is set.
5796
5927
  */
5797
5928
  function resolveUserInvocable({ rootFrontmatter, section }) {
@@ -7641,6 +7772,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7641
7772
  }
7642
7773
  }
7643
7774
  },
7775
+ ".pi/settings.json": {
7776
+ format: "json",
7777
+ invalidRootPolicy: "error",
7778
+ features: { permissions: {
7779
+ kind: "replace-owned-keys",
7780
+ ownedKeys: ["defaultTools"]
7781
+ } }
7782
+ },
7783
+ ".pi/agent/settings.json": {
7784
+ format: "json",
7785
+ invalidRootPolicy: "error",
7786
+ features: { permissions: {
7787
+ kind: "replace-owned-keys",
7788
+ ownedKeys: ["defaultTools"]
7789
+ } }
7790
+ },
7644
7791
  ".github/copilot/settings.json": {
7645
7792
  format: "json",
7646
7793
  invalidRootPolicy: "error",
@@ -7666,7 +7813,9 @@ const SHARED_CONFIG_OWNERSHIP = {
7666
7813
  ownedKeys: [
7667
7814
  "chat.tools.terminal.autoApprove",
7668
7815
  "chat.tools.edits.autoApprove",
7669
- "chat.tools.urls.autoApprove"
7816
+ "chat.tools.urls.autoApprove",
7817
+ "zoo-code.allowedCommands",
7818
+ "zoo-code.deniedCommands"
7670
7819
  ]
7671
7820
  } }
7672
7821
  },
@@ -12227,6 +12376,8 @@ const PI_RULE_FILE_NAME = "AGENTS.md";
12227
12376
  const PI_RULE_OVERRIDE_FILE_NAME = "AGENTS.override.md";
12228
12377
  const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
12229
12378
  const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
12379
+ const PI_SETTINGS_FILE_NAME = "settings.json";
12380
+ const PI_AGENT_DIR_PATH = PI_AGENT_DIR;
12230
12381
  //#endregion
12231
12382
  //#region src/features/commands/pi-command.ts
12232
12383
  /**
@@ -12628,6 +12779,21 @@ const ROO_DIR = ".roo";
12628
12779
  const ROO_COMMANDS_DIR_PATH = (0, node_path.join)(ROO_DIR, "commands");
12629
12780
  const ROO_SKILLS_DIR_PATH = (0, node_path.join)(ROO_DIR, "skills");
12630
12781
  const ROO_MCP_FILE_NAME = "mcp.json";
12782
+ /**
12783
+ * Mode slugs Roo/Zoo Code themselves accept for a `rules-{mode}` directory:
12784
+ * the loader builds the directory name by interpolating the active mode slug,
12785
+ * and custom-mode slugs are restricted to this alphabet. Validating against it
12786
+ * also keeps an authored value from escaping `.roo/` through path separators or
12787
+ * `..` segments.
12788
+ */
12789
+ const ROO_MODE_SLUG_PATTERN = /^[a-zA-Z0-9-]+$/;
12790
+ /**
12791
+ * `.roo/rules-{mode}/` — the mode-specific rule directory Roo/Zoo Code load
12792
+ * INSTEAD of `.roo/rules/` while that mode is active. The relative path is the
12793
+ * same in global scope, where it resolves under `~/.roo/`.
12794
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/prompts/sections/custom-instructions.ts
12795
+ */
12796
+ const rooModeRulesDirName = (mode) => `rules-${mode}`;
12631
12797
  const ROO_IGNORE_FILE_NAME = ".rooignore";
12632
12798
  /**
12633
12799
  * Roo Code reads project-level custom modes from a single aggregated
@@ -14151,7 +14317,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
14151
14317
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
14152
14318
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
14153
14319
  const isEmittableString = ({ value }) => isNonEmptyString(value);
14154
- const isEmittableArray = ({ value }) => isStringArray(value);
14320
+ const isEmittableArray = ({ value }) => isStringArray$1(value);
14155
14321
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
14156
14322
  const isImportableArray = ({ value }) => isSafeStringArray(value);
14157
14323
  /**
@@ -14460,7 +14626,7 @@ function isStringRecord(value) {
14460
14626
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
14461
14627
  return Object.values(value).every((v) => typeof v === "string");
14462
14628
  }
14463
- function isStringArray(value) {
14629
+ function isStringArray$1(value) {
14464
14630
  return Array.isArray(value) && value.every((v) => typeof v === "string");
14465
14631
  }
14466
14632
  /** Compare object values without letting key order decide the answer. */
@@ -14492,7 +14658,7 @@ function isSafeStringRecord(value) {
14492
14658
  * skipped wholesale when that read fails.
14493
14659
  */
14494
14660
  function isSafeStringArray(value) {
14495
- return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14661
+ return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14496
14662
  }
14497
14663
  /**
14498
14664
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -14534,7 +14700,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
14534
14700
  return {
14535
14701
  ...url !== void 0 && { url },
14536
14702
  ...isStringRecord(headers) && { headers },
14537
- ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14703
+ ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14538
14704
  };
14539
14705
  }
14540
14706
  if (hookType === "mcp_tool") {
@@ -15517,7 +15683,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
15517
15683
  function parseManifest(fileContent) {
15518
15684
  try {
15519
15685
  const parsed = JSON.parse(fileContent);
15520
- if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
15686
+ if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
15521
15687
  return {
15522
15688
  generatedBy: "rulesync",
15523
15689
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -17152,6 +17318,7 @@ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "po
17152
17318
  const HERMESAGENT_FAIL_CLOSED_EVENT = "pre_tool_call";
17153
17319
  const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
17154
17320
  const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
17321
+ const HERMESAGENT_SHELL_UNSUPPORTED_EVENTS = new Set(HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS);
17155
17322
  /**
17156
17323
  * Whether an entry of the `hooks:` mapping is a hook-event list rather than one
17157
17324
  * of its non-event siblings.
@@ -17240,7 +17407,8 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
17240
17407
  }
17241
17408
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
17242
17409
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
17243
- 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.`);
17410
+ 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.`);
17411
+ 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.`);
17244
17412
  setHermesHookEntries({
17245
17413
  result,
17246
17414
  event: nativeEvent,
@@ -22364,7 +22532,7 @@ function isCodexStdioServer(config) {
22364
22532
  * @see https://learn.chatgpt.com/docs/extend/mcp
22365
22533
  */
22366
22534
  function isValidRenamedArray(key, value) {
22367
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
22535
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
22368
22536
  }
22369
22537
  /**
22370
22538
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
@@ -22884,6 +23052,49 @@ const resolveCopilotcliServerType = (server) => {
22884
23052
  return isRemoteMcpServer(server) ? "http" : "stdio";
22885
23053
  };
22886
23054
  /**
23055
+ * Copilot CLI's per-server tool allowlist. `tools` selects which of the
23056
+ * server's tools are exposed — `["*"]` (the default) for all, or a list of
23057
+ * names — so the canonical `enabledTools` is written under that name rather
23058
+ * than passed through as a key the CLI ignores.
23059
+ *
23060
+ * When a server carries both, the canonical `enabledTools` wins and the native
23061
+ * `tools` is dropped with a warning. rulesync owns the generated allowlist, and
23062
+ * the `tools` value most likely present is the upstream default `["*"]` — left
23063
+ * to win, it would silently discard a real allowlist in favour of "expose every
23064
+ * tool", which is the failure this mapping exists to prevent. That is the
23065
+ * "canonical wins" branch of the collision rule documented on `enabledTools` in
23066
+ * `src/types/mcp.ts`; `kiro-mcp.ts` merges instead (its native names are
23067
+ * redundant spellings of additive lists) and `codexcli-mcp.ts` refuses instead
23068
+ * (Codex's same-named key means a different thing).
23069
+ *
23070
+ * `disabledTools` has no counterpart upstream (expressing it would need the
23071
+ * server's full tool list), so the target keeps `supportsDisabledTools: false`
23072
+ * and the processor strips it before it reaches here.
23073
+ *
23074
+ * @see https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
23075
+ */
23076
+ function renameEnabledToolsToTools({ server, serverName, logger }) {
23077
+ const { enabledTools, ...rest } = server;
23078
+ if (enabledTools === void 0) return rest;
23079
+ if (rest.tools !== void 0) logger?.warn(`[CopilotcliMcp] MCP server "${serverName}" declares both 'tools' and 'enabledTools'; keeping 'enabledTools' and dropping 'tools'.`);
23080
+ return {
23081
+ ...rest,
23082
+ tools: enabledTools
23083
+ };
23084
+ }
23085
+ /**
23086
+ * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
23087
+ * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
23088
+ * CLI-written entry can be a bare string. Canonical `enabledTools` is
23089
+ * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
23090
+ * through would reject the WHOLE imported MCP file rather than one key. Anything
23091
+ * that is not a string array is therefore left under `tools` untouched, matching
23092
+ * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
23093
+ */
23094
+ function isStringArray(value) {
23095
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
23096
+ }
23097
+ /**
22887
23098
  * Resolves and sets the transport type for each MCP server config.
22888
23099
  * GitHub Copilot CLI requires the "type" field for each server. A server it
22889
23100
  * cannot express — one that names no transport, or names one it carries no way
@@ -22927,7 +23138,11 @@ function addTypeField(mcpServers, logger) {
22927
23138
  }
22928
23139
  const { httpUrl: _httpUrl, ...rest } = parsed;
22929
23140
  result[name] = {
22930
- ...rest,
23141
+ ...renameEnabledToolsToTools({
23142
+ server: rest,
23143
+ serverName: name,
23144
+ logger
23145
+ }),
22931
23146
  type,
22932
23147
  url
22933
23148
  };
@@ -22945,7 +23160,11 @@ function addTypeField(mcpServers, logger) {
22945
23160
  continue;
22946
23161
  }
22947
23162
  result[name] = {
22948
- ...local,
23163
+ ...renameEnabledToolsToTools({
23164
+ server: local,
23165
+ serverName: name,
23166
+ logger
23167
+ }),
22949
23168
  type,
22950
23169
  command: head,
22951
23170
  ...tail.length > 0 && { args: tail }
@@ -22959,11 +23178,19 @@ function addTypeField(mcpServers, logger) {
22959
23178
  function removeTypeField(config) {
22960
23179
  const result = {};
22961
23180
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
22962
- if (server.type !== "stdio") {
22963
- result[name] = server;
23181
+ const { tools, ...withoutTools } = server;
23182
+ const restored = isStringArray(tools) ? {
23183
+ ...withoutTools,
23184
+ enabledTools: tools
23185
+ } : {
23186
+ ...withoutTools,
23187
+ ...tools !== void 0 && { tools }
23188
+ };
23189
+ if (restored.type !== "stdio") {
23190
+ result[name] = restored;
22964
23191
  continue;
22965
23192
  }
22966
- const { type: _, ...rest } = server;
23193
+ const { type: _, ...rest } = restored;
22967
23194
  result[name] = rest;
22968
23195
  }
22969
23196
  return result;
@@ -23615,11 +23842,11 @@ function applyGooseStdioFields(ext, config) {
23615
23842
  if (Array.isArray(command)) {
23616
23843
  if (typeof command[0] === "string") ext.cmd = command[0];
23617
23844
  const rest = command.slice(1).filter((c) => typeof c === "string");
23618
- const args = isStringArray$1(config.args) ? config.args : [];
23845
+ const args = isStringArray$2(config.args) ? config.args : [];
23619
23846
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
23620
23847
  } else if (typeof command === "string") {
23621
23848
  ext.cmd = command;
23622
- if (isStringArray$1(config.args)) ext.args = config.args;
23849
+ if (isStringArray$2(config.args)) ext.args = config.args;
23623
23850
  }
23624
23851
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
23625
23852
  }
@@ -23721,7 +23948,7 @@ function convertFromGooseFormat(extensions) {
23721
23948
  else if (type === "streamable_http") server.type = "http";
23722
23949
  else server.type = "stdio";
23723
23950
  if (typeof ext.cmd === "string") server.command = ext.cmd;
23724
- if (isStringArray$1(ext.args)) server.args = ext.args;
23951
+ if (isStringArray$2(ext.args)) server.args = ext.args;
23725
23952
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
23726
23953
  if (typeof ext.uri === "string") server.url = ext.uri;
23727
23954
  if (isPlainObject$1(ext.headers)) server.headers = omitPrototypePollutionKeys(ext.headers);
@@ -23742,11 +23969,11 @@ function buildGoosePluginStdioServer(config) {
23742
23969
  if (Array.isArray(command)) {
23743
23970
  if (typeof command[0] === "string") server.command = command[0];
23744
23971
  const rest = command.slice(1).filter((c) => typeof c === "string");
23745
- const args = isStringArray$1(config.args) ? config.args : [];
23972
+ const args = isStringArray$2(config.args) ? config.args : [];
23746
23973
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
23747
23974
  } else if (typeof command === "string") {
23748
23975
  server.command = command;
23749
- if (isStringArray$1(config.args)) server.args = config.args;
23976
+ if (isStringArray$2(config.args)) server.args = config.args;
23750
23977
  }
23751
23978
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
23752
23979
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -24078,7 +24305,7 @@ function copyHermesOauth(source) {
24078
24305
  "client_secret"
24079
24306
  ]) if (typeof source[key] === "string") oauth[key] = source[key];
24080
24307
  if (typeof source.redirect_port === "number") oauth.redirect_port = source.redirect_port;
24081
- if (isStringArray$1(source.scopes)) oauth.scopes = source.scopes;
24308
+ if (isStringArray$2(source.scopes)) oauth.scopes = source.scopes;
24082
24309
  return Object.keys(oauth).length > 0 ? oauth : void 0;
24083
24310
  }
24084
24311
  function copyHermesAdvancedFields(source, target) {
@@ -24087,7 +24314,7 @@ function copyHermesAdvancedFields(source, target) {
24087
24314
  target.auth = source.auth;
24088
24315
  copied = true;
24089
24316
  }
24090
- if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
24317
+ if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
24091
24318
  target.client_cert = source.client_cert;
24092
24319
  copied = true;
24093
24320
  }
@@ -24156,8 +24383,8 @@ function copyHermesAdvancedFields(source, target) {
24156
24383
  */
24157
24384
  function buildHermesToolsBlock(config) {
24158
24385
  const tools = {};
24159
- if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
24160
- if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
24386
+ if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
24387
+ if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
24161
24388
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
24162
24389
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
24163
24390
  return tools;
@@ -24169,8 +24396,8 @@ function buildHermesToolsBlock(config) {
24169
24396
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
24170
24397
  */
24171
24398
  function applyHermesToolsBlock(hermesTools, server) {
24172
- if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
24173
- if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24399
+ if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
24400
+ if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24174
24401
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
24175
24402
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
24176
24403
  }
@@ -24196,11 +24423,11 @@ function convertServerToHermes(config) {
24196
24423
  if (Array.isArray(command)) {
24197
24424
  if (typeof command[0] === "string") out.command = command[0];
24198
24425
  const rest = command.slice(1).filter((c) => typeof c === "string");
24199
- const args = isStringArray$1(config.args) ? config.args : [];
24426
+ const args = isStringArray$2(config.args) ? config.args : [];
24200
24427
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
24201
24428
  } else if (typeof command === "string") {
24202
24429
  out.command = command;
24203
- if (isStringArray$1(config.args)) out.args = config.args;
24430
+ if (isStringArray$2(config.args)) out.args = config.args;
24204
24431
  }
24205
24432
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
24206
24433
  } else if (url !== void 0) {
@@ -24250,7 +24477,7 @@ function convertFromHermesFormat(mcpServers) {
24250
24477
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
24251
24478
  const server = {};
24252
24479
  if (typeof config.command === "string") server.command = config.command;
24253
- if (isStringArray$1(config.args)) server.args = config.args;
24480
+ if (isStringArray$2(config.args)) server.args = config.args;
24254
24481
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
24255
24482
  if (typeof config.url === "string") server.url = config.url;
24256
24483
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -25208,13 +25435,17 @@ function mergeToolLists(...lists) {
25208
25435
  *
25209
25436
  * Both native names are documented per-server fields, so a config that already
25210
25437
  * spells them natively keeps working: the two lists are merged rather than
25211
- * one overwriting the other.
25438
+ * one overwriting the other. That is the "merge" branch of the collision rule
25439
+ * documented on `enabledTools` in `src/types/mcp.ts` — it applies here because
25440
+ * the native names are redundant spellings of the same additive lists;
25441
+ * `copilotcli-mcp.ts` lets the canonical value win instead, and
25442
+ * `codexcli-mcp.ts` refuses the canonical value instead.
25212
25443
  * @see https://kiro.dev/docs/mcp/configuration/
25213
25444
  */
25214
25445
  function toKiroMcpServers(servers) {
25215
25446
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25216
25447
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
25217
- const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25448
+ const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25218
25449
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
25219
25450
  return [name, {
25220
25451
  ...rest,
@@ -25238,7 +25469,7 @@ function fromKiroMcpServers(servers) {
25238
25469
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25239
25470
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
25240
25471
  const { autoApprove, ...rest } = server;
25241
- if (!isStringArray$1(autoApprove)) return [name, server];
25472
+ if (!isStringArray$2(autoApprove)) return [name, server];
25242
25473
  return [name, {
25243
25474
  ...rest,
25244
25475
  kiroAutoApprove: autoApprove
@@ -26380,7 +26611,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
26380
26611
  }
26381
26612
  function disabledNamesOf(config) {
26382
26613
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
26383
- return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26614
+ return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26384
26615
  }
26385
26616
  /**
26386
26617
  * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
@@ -26513,7 +26744,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
26513
26744
  return [];
26514
26745
  }
26515
26746
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
26516
- const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26747
+ const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26517
26748
  const managedNameSet = new Set(managedNames);
26518
26749
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
26519
26750
  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.`);
@@ -27355,7 +27586,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27355
27586
  meta: {
27356
27587
  supportsProject: true,
27357
27588
  supportsGlobal: true,
27358
- supportsEnabledTools: false,
27589
+ supportsEnabledTools: true,
27359
27590
  supportsDisabledTools: false
27360
27591
  }
27361
27592
  }],
@@ -27588,6 +27819,27 @@ const defaultGetFactory$3 = (target) => {
27588
27819
  if (!factory) throw new Error(`Unsupported tool target: ${target}`);
27589
27820
  return factory;
27590
27821
  };
27822
+ /**
27823
+ * Warn about per-server tool filters the target tool cannot express.
27824
+ *
27825
+ * `enabledTools`/`disabledTools` are canonical fields, so a single
27826
+ * `.rulesync/.mcp.json` can carry a filter that only some targets read. The
27827
+ * unsupported ones are stripped before generation (writing them would produce a
27828
+ * key the tool discards on load), which used to happen silently — the filter
27829
+ * simply did not apply and nothing said so. Warning names the servers whose
27830
+ * filter is being dropped for this target so the gap is visible at generate
27831
+ * time rather than in the tool's behavior.
27832
+ *
27833
+ * Only fields actually present are reported, so a config that never authored
27834
+ * the filter stays quiet.
27835
+ */
27836
+ function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger }) {
27837
+ for (const field of fields) {
27838
+ const serverNames = Object.entries(mcpServers ?? {}).filter(([, serverConfig]) => serverConfig[field] !== void 0).map(([serverName]) => serverName);
27839
+ if (serverNames.length === 0) continue;
27840
+ logger.warn(`${toolTarget} does not read the per-server \`${field}\` MCP tool filter; dropping it from ${serverNames.join(", ")}.`);
27841
+ }
27842
+ }
27591
27843
  var McpProcessor = class extends FeatureProcessor {
27592
27844
  toolTarget;
27593
27845
  global;
@@ -27667,6 +27919,12 @@ var McpProcessor = class extends FeatureProcessor {
27667
27919
  const fieldsToStrip = [];
27668
27920
  if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
27669
27921
  if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
27922
+ warnStrippedMcpServerFields({
27923
+ mcpServers: targetedRulesyncMcp.getJson().mcpServers,
27924
+ fields: fieldsToStrip,
27925
+ toolTarget: this.toolTarget,
27926
+ logger: this.logger
27927
+ });
27670
27928
  const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
27671
27929
  return await factory.class.fromRulesyncMcp({
27672
27930
  outputRoot: this.outputRoot,
@@ -28179,6 +28437,34 @@ function convertAmpToRulesync({ disable, permissions }) {
28179
28437
  //#endregion
28180
28438
  //#region src/features/permissions/antigravity-cli-permissions.ts
28181
28439
  /**
28440
+ * Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
28441
+ * permissions override may author. They are siblings of `permissions`, passed
28442
+ * through verbatim in both directions.
28443
+ */
28444
+ const ANTIGRAVITY_CLI_OVERRIDE_KEYS = [
28445
+ "toolPermission",
28446
+ "enableTerminalSandbox",
28447
+ "artifactReviewPolicy",
28448
+ "allowNonWorkspaceAccess",
28449
+ "agentMode"
28450
+ ];
28451
+ const moduleLogger$2 = fallbackLogger;
28452
+ /**
28453
+ * Narrow an imported `settings.json` value to one of the documented values of
28454
+ * an enum-typed override key. The override schema declares `toolPermission`,
28455
+ * `artifactReviewPolicy` and `agentMode` as `z.enum`, so carrying an
28456
+ * unrecognized value through — a hand-written typo, or a preset a newer
28457
+ * Antigravity release added — would fail validation of the whole imported
28458
+ * `.rulesync/permissions.jsonc` instead of dropping just that key. A dropped
28459
+ * value is warned about rather than discarded silently, so a preset rulesync
28460
+ * does not know about yet is visible instead of quietly missing.
28461
+ */
28462
+ function pickDocumentedValue({ key, value, documented }) {
28463
+ if (typeof value !== "string") return void 0;
28464
+ if (documented.includes(value)) return value;
28465
+ 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.`);
28466
+ }
28467
+ /**
28182
28468
  * Deliberate mapping from rulesync canonical permission categories to the
28183
28469
  * Antigravity CLI Fine-Grained Permissions Engine action vocabulary
28184
28470
  * (`read_file`, `write_file`, `read_url`, `command`, `mcp`, plus `execute_url` /
@@ -28253,9 +28539,10 @@ function buildPermissionEntry$1(toolName, pattern) {
28253
28539
  * file (global scope only). The file holds other CLI settings besides
28254
28540
  * permissions, so it is never deleted.
28255
28541
  *
28256
- * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28542
+ * Five CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28257
28543
  * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
28258
- * `artifactReviewPolicy` and `allowNonWorkspaceAccess` — are authored and
28544
+ * `artifactReviewPolicy`, `allowNonWorkspaceAccess` and `agentMode` (the
28545
+ * baseline execution mode) — are authored and
28259
28546
  * round-tripped through the `antigravity-cli` permissions override (see
28260
28547
  * `AntigravityCliPermissionsOverrideSchema`).
28261
28548
  */
@@ -28319,10 +28606,10 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28319
28606
  permissions: mergedPermissions
28320
28607
  };
28321
28608
  const override = config["antigravity-cli"];
28322
- if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
28323
- if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
28324
- if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
28325
- if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
28609
+ for (const key of ANTIGRAVITY_CLI_OVERRIDE_KEYS) {
28610
+ const value = override?.[key];
28611
+ if (value !== void 0) merged[key] = value;
28612
+ }
28326
28613
  const fileContent = JSON.stringify(merged, null, 2);
28327
28614
  return new AntigravityCliPermissions({
28328
28615
  outputRoot,
@@ -28347,10 +28634,26 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28347
28634
  deny: permissions.deny ?? []
28348
28635
  });
28349
28636
  const override = {};
28350
- if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
28637
+ const toolPermission = pickDocumentedValue({
28638
+ key: "toolPermission",
28639
+ value: settings.toolPermission,
28640
+ documented: ANTIGRAVITY_CLI_TOOL_PERMISSIONS
28641
+ });
28642
+ if (toolPermission !== void 0) override.toolPermission = toolPermission;
28351
28643
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
28352
- if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
28644
+ const artifactReviewPolicy = pickDocumentedValue({
28645
+ key: "artifactReviewPolicy",
28646
+ value: settings.artifactReviewPolicy,
28647
+ documented: ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES
28648
+ });
28649
+ if (artifactReviewPolicy !== void 0) override.artifactReviewPolicy = artifactReviewPolicy;
28353
28650
  if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
28651
+ const agentMode = pickDocumentedValue({
28652
+ key: "agentMode",
28653
+ value: settings.agentMode,
28654
+ documented: ANTIGRAVITY_CLI_AGENT_MODES
28655
+ });
28656
+ if (agentMode !== void 0) override.agentMode = agentMode;
28354
28657
  const result = { ...config };
28355
28658
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
28356
28659
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -29176,6 +29479,119 @@ function deepMergeRecords(base, patch) {
29176
29479
  }
29177
29480
  return merged;
29178
29481
  }
29482
+ /**
29483
+ * `sandbox.*` paths Claude Code honors only from user settings, managed settings
29484
+ * and the `--settings` CLI flag. Written into a project `.claude/settings.json`
29485
+ * they are silently ignored, so a committed file would read as though it
29486
+ * enforced a sandbox policy while doing nothing — security-relevant for
29487
+ * `network.strictAllowlist` and the credential-masking keys in particular.
29488
+ * Generation therefore drops them at project scope with a per-key warning and
29489
+ * emits them only under `--global`.
29490
+ *
29491
+ * Deliberately NOT listed:
29492
+ * - `bwrapPath` / `socatPath`: v2.1.232 added them to the managed-settings
29493
+ * approval dialog, which is a consent prompt, not a project-scope rejection.
29494
+ * - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
29495
+ * unit is the individual entry's mode, not the settings key, and the same
29496
+ * lists carry `deny` entries that project settings *do* honor — dropping a
29497
+ * whole list would remove real restrictions. `stripProjectIgnoredMaskEntries`
29498
+ * filters those lists per entry instead.
29499
+ *
29500
+ * @see https://code.claude.com/docs/en/sandboxing
29501
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
29502
+ */
29503
+ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29504
+ ["filesystem", "disabled"],
29505
+ ["network", "strictAllowlist"],
29506
+ ["network", "tlsTerminate"],
29507
+ ["credentials", "allowPlaintextInject"],
29508
+ ["credentials", "awsPairs"],
29509
+ ["credentials", "sigv4"],
29510
+ ["allowAppleEvents"],
29511
+ ["ripgrep"]
29512
+ ];
29513
+ /**
29514
+ * Copy of the authored `sandbox` override with the user/managed-only paths
29515
+ * removed, warning once per dropped path. Only the override copy is filtered —
29516
+ * a value already hand-written in the target file is left untouched, matching
29517
+ * the `qwencode` `security.allowPrivateNetworkHooks` precedent.
29518
+ */
29519
+ function stripGlobalOnlySandboxPaths({ sandbox, relativeFilePath, logger }) {
29520
+ const filtered = structuredClone(sandbox);
29521
+ for (const path of CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS) {
29522
+ const leaf = path.at(-1);
29523
+ if (leaf === void 0) continue;
29524
+ const parentPath = path.slice(0, -1);
29525
+ let parent = filtered;
29526
+ for (const segment of parentPath) {
29527
+ const next = parent[segment];
29528
+ if (!isPlainRecord(next)) {
29529
+ parent = {};
29530
+ break;
29531
+ }
29532
+ parent = next;
29533
+ }
29534
+ if (parent[leaf] === void 0) continue;
29535
+ delete parent[leaf];
29536
+ const [container] = parentPath;
29537
+ if (container !== void 0 && isPlainRecord(filtered[container])) {
29538
+ if (Object.keys(filtered[container]).length === 0) delete filtered[container];
29539
+ }
29540
+ 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.`);
29541
+ }
29542
+ return filtered;
29543
+ }
29544
+ /** The `sandbox.credentials` lists whose entries accept `"mode": "mask"`. */
29545
+ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
29546
+ /**
29547
+ * Copy of the authored `sandbox` override with the `credentials.envVars` /
29548
+ * `credentials.files` entries whose `mode` is `"mask"` removed, warning once per
29549
+ * list.
29550
+ *
29551
+ * Masking authorizes the sandbox proxy to send the real credential to the listed
29552
+ * hosts, so Claude Code honors it only from user settings, managed settings and
29553
+ * the `--settings` CLI flag; a `mask` entry in a repository's
29554
+ * `.claude/settings.json` is ignored outright. Keeping it would read as though
29555
+ * the credential were masked while nothing protects it — the one project-scope
29556
+ * gap whose consequence is a live credential leaving the sandbox unmasked.
29557
+ *
29558
+ * Filtered per entry rather than per key, because the same lists carry `deny`
29559
+ * entries that project settings *do* honor. `mode` is matched exactly: an entry
29560
+ * Claude Code cannot read as `mask` is not treated as one either (it degrades
29561
+ * such entries to `deny`), so the "reads as masked but isn't" state this guards
29562
+ * against cannot slip through a differently-spelled value.
29563
+ *
29564
+ * Like `stripGlobalOnlySandboxPaths`, only the override copy is filtered — a
29565
+ * value already in the target file is left untouched, which is why the warning
29566
+ * points at it.
29567
+ *
29568
+ * @see https://code.claude.com/docs/en/sandboxing — "`mask` entries … are all
29569
+ * ignored in a repository's `.claude/settings.json` or
29570
+ * `.claude/settings.local.json`"; the file-entry section states the same
29571
+ * settings-source restriction applies there.
29572
+ */
29573
+ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
29574
+ const credentials = sandbox.credentials;
29575
+ if (!isPlainRecord(credentials)) return sandbox;
29576
+ const filteredCredentials = { ...credentials };
29577
+ let changed = false;
29578
+ for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
29579
+ const list = filteredCredentials[listKey];
29580
+ if (!Array.isArray(list)) continue;
29581
+ const kept = list.filter((entry) => !(isPlainRecord(entry) && entry.mode === "mask"));
29582
+ if (kept.length === list.length) continue;
29583
+ changed = true;
29584
+ const dropped = list.length - kept.length;
29585
+ 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.`);
29586
+ if (kept.length === 0) delete filteredCredentials[listKey];
29587
+ else filteredCredentials[listKey] = kept;
29588
+ }
29589
+ if (!changed) return sandbox;
29590
+ const filtered = { ...sandbox };
29591
+ if (Object.keys(filteredCredentials).length === 0) delete filtered.credentials;
29592
+ else filtered.credentials = filteredCredentials;
29593
+ return filtered;
29594
+ }
29179
29595
  const CLAUDE_PATH_RULE_ALIASES = {
29180
29596
  Write: "Edit",
29181
29597
  NotebookEdit: "Edit",
@@ -29229,7 +29645,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29229
29645
  validate
29230
29646
  });
29231
29647
  }
29232
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
29648
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
29233
29649
  const paths = ClaudecodePermissions.getSettablePaths();
29234
29650
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
29235
29651
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -29253,7 +29669,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29253
29669
  };
29254
29670
  }
29255
29671
  const overrideSandbox = config.claudecode?.sandbox;
29256
- if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
29672
+ if (isPlainRecord(overrideSandbox)) {
29673
+ const scopedSandbox = global ? overrideSandbox : stripProjectIgnoredMaskEntries({
29674
+ sandbox: stripGlobalOnlySandboxPaths({
29675
+ sandbox: overrideSandbox,
29676
+ relativeFilePath: paths.relativeFilePath,
29677
+ logger
29678
+ }),
29679
+ relativeFilePath: paths.relativeFilePath,
29680
+ logger
29681
+ });
29682
+ if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
29683
+ }
29257
29684
  const managedToolNames = managedClaudeToolNames(config);
29258
29685
  const merged = applyPermissions({
29259
29686
  settings,
@@ -31430,7 +31857,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
31430
31857
  function convertGoosePermissionConfigToRulesync(userPermission) {
31431
31858
  const permission = {};
31432
31859
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
31433
- const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
31860
+ const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
31434
31861
  const action = GOOSE_LIST_TO_ACTION[key];
31435
31862
  for (const toolName of toolNames) {
31436
31863
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
@@ -31723,7 +32150,7 @@ const ACTION_RANK = {
31723
32150
  * (mirrors the Cursor adapter's preservation of unmanaged types).
31724
32151
  */
31725
32152
  function unmanagedEntries(existingPermission, key) {
31726
- return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
32153
+ return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
31727
32154
  }
31728
32155
  /**
31729
32156
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -31766,9 +32193,9 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
31766
32193
  * described more than once resolves to the strictest action.
31767
32194
  */
31768
32195
  function parseGrokPermissionArrays(permission) {
31769
- const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
31770
- const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
31771
- const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
32196
+ const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
32197
+ const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
32198
+ const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
31772
32199
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31773
32200
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
31774
32201
  const result = {};
@@ -31950,20 +32377,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
31950
32377
  const permission = clonePermissionBlock(provenance.permission);
31951
32378
  reconcileCommandAllowlist({
31952
32379
  permission,
31953
- commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
32380
+ commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
31954
32381
  });
31955
32382
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
31956
32383
  reconcileNativeDenies({
31957
32384
  permission,
31958
32385
  category: "bash",
31959
- patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
32386
+ patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
31960
32387
  });
31961
32388
  const security = isRecord$1(config.security) ? config.security : {};
31962
32389
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
31963
32390
  reconcileNativeDenies({
31964
32391
  permission,
31965
32392
  category: "webfetch",
31966
- patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
32393
+ patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
31967
32394
  });
31968
32395
  removeEmptyCategories(permission);
31969
32396
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -33151,7 +33578,7 @@ function extractKiroOverride(toolsSettings) {
33151
33578
  if (Object.keys(overrideToolsSettings).length === 0) return void 0;
33152
33579
  return { toolsSettings: overrideToolsSettings };
33153
33580
  }
33154
- function asStringArray(value) {
33581
+ function asStringArray$1(value) {
33155
33582
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
33156
33583
  }
33157
33584
  /**
@@ -33161,8 +33588,8 @@ function asStringArray(value) {
33161
33588
  */
33162
33589
  function rulesFromArrays(settings, allowKey, denyKey) {
33163
33590
  const rules = {};
33164
- for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
33165
- for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
33591
+ for (const pattern of asStringArray$1(settings[allowKey])) rules[pattern] = "allow";
33592
+ for (const pattern of asStringArray$1(settings[denyKey])) rules[pattern] = "deny";
33166
33593
  return rules;
33167
33594
  }
33168
33595
  //#endregion
@@ -33391,6 +33818,115 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
33391
33818
  }
33392
33819
  };
33393
33820
  //#endregion
33821
+ //#region src/features/permissions/pi-permissions.ts
33822
+ /** The one `settings.json` key this feature owns. */
33823
+ const DEFAULT_TOOLS_KEY = "defaultTools";
33824
+ /**
33825
+ * Permissions generator for Pi Coding Agent.
33826
+ *
33827
+ * Pi exposes no allow/ask/deny rule surface, so no canonical permission
33828
+ * category maps onto it. Its one repository-syncable tool gate is `defaultTools` —
33829
+ * the built-in tools enabled at startup (v0.84.2) — authored through the `pi`
33830
+ * permissions override.
33831
+ *
33832
+ * Scope semantics are the **opposite** of the union-merge most targets use:
33833
+ * a project `defaultTools` array *replaces* the global array rather than adding
33834
+ * to it, so the two scopes are written independently and never combined.
33835
+ * Project scope writes `.pi/settings.json`; global scope writes
33836
+ * `~/.pi/agent/settings.json`.
33837
+ *
33838
+ * The file is hand-edited and holds many unrelated keys (`theme`,
33839
+ * `defaultModel`, `packages`, `sessionDir`, ...), so writes go through the
33840
+ * shared-config gateway with `defaultTools` as the only owned key, and the file
33841
+ * is never deleted.
33842
+ *
33843
+ * @see https://pi.dev/docs/latest/settings
33844
+ */
33845
+ var PiPermissions = class PiPermissions extends ToolPermissions {
33846
+ constructor(params) {
33847
+ super({
33848
+ ...params,
33849
+ fileContent: params.fileContent ?? "{}"
33850
+ });
33851
+ }
33852
+ /** `settings.json` holds unrelated user settings, so it must not be deleted. */
33853
+ isDeletable() {
33854
+ return false;
33855
+ }
33856
+ static getSettablePaths({ global = false } = {}) {
33857
+ return {
33858
+ relativeDirPath: global ? PI_AGENT_DIR_PATH : ".pi",
33859
+ relativeFilePath: PI_SETTINGS_FILE_NAME
33860
+ };
33861
+ }
33862
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33863
+ const paths = PiPermissions.getSettablePaths({ global });
33864
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
33865
+ return new PiPermissions({
33866
+ outputRoot,
33867
+ relativeDirPath: paths.relativeDirPath,
33868
+ relativeFilePath: paths.relativeFilePath,
33869
+ fileContent,
33870
+ validate,
33871
+ global
33872
+ });
33873
+ }
33874
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
33875
+ const paths = PiPermissions.getSettablePaths({ global });
33876
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
33877
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
33878
+ const defaultTools = rulesyncPermissions.getJson().pi?.defaultTools;
33879
+ const patch = {};
33880
+ if (defaultTools !== void 0) patch[DEFAULT_TOOLS_KEY] = defaultTools;
33881
+ return new PiPermissions({
33882
+ outputRoot,
33883
+ relativeDirPath: paths.relativeDirPath,
33884
+ relativeFilePath: paths.relativeFilePath,
33885
+ fileContent: applySharedConfigPatch({
33886
+ fileKey: sharedConfigFileKey(paths),
33887
+ feature: "permissions",
33888
+ existingContent,
33889
+ patch,
33890
+ filePath
33891
+ }),
33892
+ validate: true,
33893
+ global
33894
+ });
33895
+ }
33896
+ toRulesyncPermissions() {
33897
+ const defaultTools = this.parseSettings()[DEFAULT_TOOLS_KEY];
33898
+ const config = { permission: {} };
33899
+ if (Array.isArray(defaultTools) && defaultTools.every((tool) => typeof tool === "string")) config.pi = { [DEFAULT_TOOLS_KEY]: defaultTools };
33900
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
33901
+ }
33902
+ parseSettings() {
33903
+ try {
33904
+ return parseSharedConfig({
33905
+ format: "json",
33906
+ fileContent: this.getFileContent()
33907
+ });
33908
+ } catch (error) {
33909
+ throw new Error(`Failed to parse Pi settings in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
33910
+ }
33911
+ }
33912
+ validate() {
33913
+ return {
33914
+ success: true,
33915
+ error: null
33916
+ };
33917
+ }
33918
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
33919
+ return new PiPermissions({
33920
+ outputRoot,
33921
+ relativeDirPath,
33922
+ relativeFilePath,
33923
+ fileContent: "{}",
33924
+ validate: false,
33925
+ global
33926
+ });
33927
+ }
33928
+ };
33929
+ //#endregion
33394
33930
  //#region src/features/permissions/qwencode-permissions.ts
33395
33931
  /**
33396
33932
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -34454,7 +34990,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
34454
34990
  permission[category] ??= {};
34455
34991
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
34456
34992
  }
34457
- if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34993
+ if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34458
34994
  permission.read ??= {};
34459
34995
  permission.read[path] ??= "allow";
34460
34996
  }
@@ -35431,8 +35967,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
35431
35967
  const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
35432
35968
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
35433
35969
  const config = convertWarpToRulesyncPermissions({
35434
- allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35435
- deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35970
+ allow: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35971
+ deny: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35436
35972
  });
35437
35973
  const warpOverride = {};
35438
35974
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -35965,6 +36501,181 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
35965
36501
  }
35966
36502
  };
35967
36503
  //#endregion
36504
+ //#region src/constants/zoocode-paths.ts
36505
+ /**
36506
+ * Zoo Code is a VS Code extension, so its committable command allow/deny lists
36507
+ * are workspace settings rather than files in the `.roo/` agent-asset tree that
36508
+ * the other Zoo Code features write.
36509
+ *
36510
+ * `zoo-code.allowedCommands` / `zoo-code.deniedCommands` are contributed with no
36511
+ * `scope`, which in VS Code means `window` scope — settable in a workspace's
36512
+ * `.vscode/settings.json` — and `ClineProvider.mergeCommandLists()` unions the
36513
+ * workspace values into the effective auto-approval lists.
36514
+ *
36515
+ * The `zoo-code.*` namespace is Zoo-era (the v3.74.0 rebrand); the archived Roo
36516
+ * Code lineage spelled the same settings `roo-cline.*`, so this surface is
36517
+ * deliberately not shared with the `roo` target.
36518
+ *
36519
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
36520
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/webview/ClineProvider.ts
36521
+ */
36522
+ const ZOOCODE_VSCODE_SETTINGS_DIR = ".vscode";
36523
+ const ZOOCODE_VSCODE_SETTINGS_FILE_NAME = "settings.json";
36524
+ const ZOOCODE_ALLOWED_COMMANDS_KEY = "zoo-code.allowedCommands";
36525
+ const ZOOCODE_DENIED_COMMANDS_KEY = "zoo-code.deniedCommands";
36526
+ //#endregion
36527
+ //#region src/features/permissions/zoocode-permissions.ts
36528
+ /**
36529
+ * The canonical category Zoo Code's command lists correspond to. Zoo Code gates
36530
+ * terminal command execution and nothing else through these settings, so only
36531
+ * `bash` maps; `read`/`write`/`edit`/`webfetch` have no workspace-settable
36532
+ * counterpart in the extension's contributions.
36533
+ */
36534
+ const COMMAND_CATEGORY = "bash";
36535
+ function asStringArray(value) {
36536
+ if (!Array.isArray(value)) return [];
36537
+ return value.filter((entry) => typeof entry === "string");
36538
+ }
36539
+ /**
36540
+ * Split one canonical category's rules into Zoo Code's two command lists.
36541
+ *
36542
+ * Zoo Code matches these entries as command **prefixes**: a command runs
36543
+ * without a confirmation prompt when it starts with an `allowedCommands` entry,
36544
+ * and is refused outright when it starts with a `deniedCommands` entry (deny
36545
+ * wins). `ask` is represented by listing the pattern in neither list, which
36546
+ * leaves Zoo Code's default approval prompt in charge.
36547
+ *
36548
+ * Each list is `undefined` when it would be empty, so the key is retracted from
36549
+ * the settings file rather than written as an empty array — an empty
36550
+ * `allowedCommands` and an absent one mean the same thing to Zoo Code, and the
36551
+ * absent form leaves no rulesync residue behind.
36552
+ */
36553
+ function buildCommandLists(rules) {
36554
+ const allowed = [];
36555
+ const denied = [];
36556
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowed.push(pattern);
36557
+ else if (action === "deny") denied.push(pattern);
36558
+ return {
36559
+ allowed: allowed.length > 0 ? allowed : void 0,
36560
+ denied: denied.length > 0 ? denied : void 0
36561
+ };
36562
+ }
36563
+ /**
36564
+ * Permissions generator for Zoo Code.
36565
+ *
36566
+ * Zoo Code has no policy file in its `.roo/` tree: the committable command
36567
+ * allow/deny lists are VS Code workspace settings
36568
+ * (`zoo-code.allowedCommands` / `zoo-code.deniedCommands` in
36569
+ * `.vscode/settings.json`), which `ClineProvider.mergeCommandLists()` unions
36570
+ * into the lists the auto-approval decision reads. That file is a
36571
+ * general-purpose workspace settings file with many unrelated keys, so reads
36572
+ * and writes merge into the existing JSONC (touching only the two managed keys)
36573
+ * and the file is never deleted.
36574
+ *
36575
+ * Only project scope is modeled: VS Code's user-scope `settings.json` lives at
36576
+ * a platform-dependent path outside rulesync's home-relative global model.
36577
+ *
36578
+ * The `roo` target deliberately does not get this adapter. The settings
36579
+ * namespace is Zoo-era (`roo-cline.*` before the v3.74.0 rebrand), and Roo Code
36580
+ * is EOL with its repository archived, so emitting `zoo-code.*` keys for a
36581
+ * `--targets roo` generate would write settings that Roo itself never reads.
36582
+ *
36583
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
36584
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/auto-approval/commands.ts
36585
+ */
36586
+ var ZoocodePermissions = class ZoocodePermissions extends ToolPermissions {
36587
+ constructor(params) {
36588
+ super({
36589
+ ...params,
36590
+ fileContent: params.fileContent ?? "{}"
36591
+ });
36592
+ }
36593
+ /**
36594
+ * `.vscode/settings.json` is a user-managed workspace file with unrelated
36595
+ * settings, so it must not be deleted.
36596
+ */
36597
+ isDeletable() {
36598
+ return false;
36599
+ }
36600
+ static getSettablePaths(_options = {}) {
36601
+ return {
36602
+ relativeDirPath: ZOOCODE_VSCODE_SETTINGS_DIR,
36603
+ relativeFilePath: ZOOCODE_VSCODE_SETTINGS_FILE_NAME
36604
+ };
36605
+ }
36606
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
36607
+ const paths = ZoocodePermissions.getSettablePaths();
36608
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
36609
+ return new ZoocodePermissions({
36610
+ outputRoot,
36611
+ relativeDirPath: paths.relativeDirPath,
36612
+ relativeFilePath: paths.relativeFilePath,
36613
+ fileContent,
36614
+ validate
36615
+ });
36616
+ }
36617
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions }) {
36618
+ const paths = ZoocodePermissions.getSettablePaths();
36619
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
36620
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
36621
+ const rules = rulesyncPermissions.getJson().permission[COMMAND_CATEGORY];
36622
+ const patch = {};
36623
+ if (rules !== void 0) {
36624
+ const { allowed, denied } = buildCommandLists(rules);
36625
+ patch[ZOOCODE_ALLOWED_COMMANDS_KEY] = allowed;
36626
+ patch[ZOOCODE_DENIED_COMMANDS_KEY] = denied;
36627
+ }
36628
+ return new ZoocodePermissions({
36629
+ outputRoot,
36630
+ relativeDirPath: paths.relativeDirPath,
36631
+ relativeFilePath: paths.relativeFilePath,
36632
+ fileContent: applySharedConfigPatch({
36633
+ fileKey: sharedConfigFileKey(paths),
36634
+ feature: "permissions",
36635
+ existingContent,
36636
+ patch,
36637
+ filePath
36638
+ }),
36639
+ validate: true
36640
+ });
36641
+ }
36642
+ toRulesyncPermissions() {
36643
+ let settings;
36644
+ try {
36645
+ settings = parseSharedConfig({
36646
+ format: "jsonc",
36647
+ fileContent: this.getFileContent() || "{}",
36648
+ filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
36649
+ invalidRootPolicy: "error",
36650
+ jsoncParseErrors: "error"
36651
+ });
36652
+ } catch (error) {
36653
+ throw new Error(`Failed to parse Zoo Code VS Code settings in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
36654
+ }
36655
+ const rules = {};
36656
+ for (const pattern of asStringArray(settings[ZOOCODE_ALLOWED_COMMANDS_KEY])) rules[pattern] = "allow";
36657
+ for (const pattern of asStringArray(settings[ZOOCODE_DENIED_COMMANDS_KEY])) rules[pattern] = "deny";
36658
+ const permission = {};
36659
+ if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
36660
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
36661
+ }
36662
+ validate() {
36663
+ return {
36664
+ success: true,
36665
+ error: null
36666
+ };
36667
+ }
36668
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
36669
+ return new ZoocodePermissions({
36670
+ outputRoot,
36671
+ relativeDirPath,
36672
+ relativeFilePath,
36673
+ fileContent: "{}",
36674
+ validate: false
36675
+ });
36676
+ }
36677
+ };
36678
+ //#endregion
35968
36679
  //#region src/features/permissions/permissions-processor.ts
35969
36680
  const PermissionsProcessorToolTargetSchema = zod_mini.z.enum(permissionsProcessorToolTargetTuple);
35970
36681
  const toolPermissionsFactories = /* @__PURE__ */ new Map([
@@ -36144,6 +36855,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
36144
36855
  supportsImport: true
36145
36856
  }
36146
36857
  }],
36858
+ ["pi", {
36859
+ class: PiPermissions,
36860
+ meta: {
36861
+ supportsProject: true,
36862
+ supportsGlobal: true,
36863
+ supportsImport: true
36864
+ }
36865
+ }],
36147
36866
  ["qwencode", {
36148
36867
  class: QwencodePermissions,
36149
36868
  meta: {
@@ -36199,6 +36918,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
36199
36918
  supportsGlobal: true,
36200
36919
  supportsImport: true
36201
36920
  }
36921
+ }],
36922
+ ["zoocode", {
36923
+ class: ZoocodePermissions,
36924
+ meta: {
36925
+ supportsProject: true,
36926
+ supportsGlobal: false,
36927
+ supportsImport: true
36928
+ }
36202
36929
  }]
36203
36930
  ]);
36204
36931
  var PermissionsProcessor = class extends FeatureProcessor {
@@ -38455,6 +39182,7 @@ const CursorSkillFrontmatterSchema = zod_mini.z.looseObject({
38455
39182
  description: zod_mini.z.string(),
38456
39183
  paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
38457
39184
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
39185
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
38458
39186
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
38459
39187
  });
38460
39188
  /**
@@ -38509,6 +39237,7 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38509
39237
  const cursorSection = {
38510
39238
  ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
38511
39239
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
39240
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
38512
39241
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
38513
39242
  };
38514
39243
  const rulesyncFrontmatter = {
@@ -38536,11 +39265,16 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38536
39265
  rootFrontmatter: rulesyncFrontmatter,
38537
39266
  section: cursorSection
38538
39267
  });
39268
+ const resolvedUserInvocable = resolveUserInvocable({
39269
+ rootFrontmatter: rulesyncFrontmatter,
39270
+ section: cursorSection
39271
+ });
38539
39272
  const cursorFrontmatter = {
38540
39273
  name: rulesyncFrontmatter.name,
38541
39274
  description: rulesyncFrontmatter.description,
38542
39275
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
38543
39276
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
39277
+ ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
38544
39278
  ...cursorSection?.metadata !== void 0 && { metadata: cursorSection.metadata }
38545
39279
  };
38546
39280
  return new CursorSkill({
@@ -46543,6 +47277,23 @@ var TaktSubagent = class TaktSubagent extends ToolSubagent {
46543
47277
  };
46544
47278
  //#endregion
46545
47279
  //#region src/features/subagents/vibe-subagent.ts
47280
+ /**
47281
+ * Where Vibe reads a custom system prompt from: `.vibe/prompts/<id>.md`
47282
+ * (project) and `~/.vibe/prompts/<id>.md` (user), with the custom directories
47283
+ * winning over the builtins. The id must be a bare file name with no path
47284
+ * separators.
47285
+ * @see vibe/core/prompts/__init__.py
47286
+ */
47287
+ const VIBE_PROMPTS_DIR_PATH = (0, node_path.join)(".vibe", "prompts");
47288
+ /**
47289
+ * Vibe resolves a `system_prompt_id` by file name, and
47290
+ * `VibeConfigSchema._check_system_prompt` evaluates it during validation — an
47291
+ * unresolvable id raises and the agent is dropped at discovery. The id must
47292
+ * therefore stay a bare slug with no path separators.
47293
+ */
47294
+ function promptSlug(relativeFilePath) {
47295
+ return (0, node_path.basename)(relativeFilePath).replace(/\.(md|toml)$/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
47296
+ }
46546
47297
  const VibeSubagentTomlSchema = zod_mini.z.looseObject({
46547
47298
  agent_type: zod_mini.z.enum(["agent", "subagent"]),
46548
47299
  display_name: zod_mini.z.optional(zod_mini.z.string()),
@@ -46559,14 +47310,22 @@ const VibeSubagentTomlSchema = zod_mini.z.looseObject({
46559
47310
  });
46560
47311
  var VibeSubagent = class VibeSubagent extends ToolSubagent {
46561
47312
  body;
46562
- constructor({ body, ...rest }) {
46563
- if (rest.validate !== false) try {
47313
+ promptFile;
47314
+ resolvedSystemPrompt;
47315
+ constructor({ body, promptFile = false, resolvedSystemPrompt, ...rest }) {
47316
+ if (rest.validate !== false && !promptFile) try {
46564
47317
  VibeSubagentTomlSchema.parse(smol_toml.parse(body));
46565
47318
  } catch (error) {
46566
47319
  throw new Error(`Invalid TOML in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(error)}`, { cause: error });
46567
47320
  }
46568
47321
  super({ ...rest });
46569
47322
  this.body = body;
47323
+ this.promptFile = promptFile;
47324
+ this.resolvedSystemPrompt = resolvedSystemPrompt;
47325
+ }
47326
+ /** True for the companion `.vibe/prompts/<id>.md` file. */
47327
+ isPromptFile() {
47328
+ return this.promptFile;
46570
47329
  }
46571
47330
  static getSettablePaths(_options = {}) {
46572
47331
  return { relativeDirPath: (0, node_path.join)(".vibe", "agents") };
@@ -46581,7 +47340,9 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46581
47340
  } catch (error) {
46582
47341
  throw new Error(`Failed to parse TOML in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
46583
47342
  }
46584
- const { system_prompt, description, display_name, ...vibeSection } = parsed;
47343
+ const { system_prompt, system_prompt_id, description, display_name, ...vibeSection } = parsed;
47344
+ const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
47345
+ const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
46585
47346
  const fileStem = (0, node_path.basename)(this.getRelativeFilePath(), ".toml");
46586
47347
  const rulesyncFrontmatter = {
46587
47348
  targets: ["vibe"],
@@ -46590,13 +47351,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46590
47351
  vibe: {
46591
47352
  ...display_name !== void 0 && { display_name },
46592
47353
  ...description !== void 0 && { description },
47354
+ ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
46593
47355
  ...vibeSection
46594
47356
  }
46595
47357
  };
46596
47358
  return new RulesyncSubagent({
46597
47359
  outputRoot: this.outputRoot,
46598
47360
  frontmatter: rulesyncFrontmatter,
46599
- body: system_prompt ?? "",
47361
+ body: resolvedBody ?? "",
46600
47362
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
46601
47363
  relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
46602
47364
  validate: true
@@ -46611,12 +47373,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46611
47373
  "description",
46612
47374
  "system_prompt"
46613
47375
  ]);
47376
+ const promptBody = rulesyncSubagent.getBody();
47377
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
46614
47378
  const tomlObj = {
46615
47379
  agent_type: rawSection.agent_type === "agent" ? "agent" : "subagent",
46616
47380
  display_name: typeof rawSection.display_name === "string" ? rawSection.display_name : frontmatter.name,
46617
47381
  ...frontmatter.description ? { description: frontmatter.description } : {},
46618
- ...rulesyncSubagent.getBody() ? { system_prompt: rulesyncSubagent.getBody() } : {},
46619
- ...vibeSection
47382
+ ...vibeSection,
47383
+ ...promptBody ? { system_prompt_id: slug } : {}
46620
47384
  };
46621
47385
  const body = smol_toml.stringify(tomlObj);
46622
47386
  const paths = this.getSettablePaths({ global });
@@ -46631,6 +47395,36 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46631
47395
  global
46632
47396
  });
46633
47397
  }
47398
+ /**
47399
+ * Emits the agent TOML plus its companion prompt file. The prompt file MUST
47400
+ * be written together with the id: `VibeConfigSchema._check_system_prompt`
47401
+ * evaluates the property during validation, so an unresolvable
47402
+ * `system_prompt_id` raises and `AgentRegistry._try_load` drops the agent
47403
+ * with a warning.
47404
+ */
47405
+ static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false }) {
47406
+ return rulesyncSubagents.flatMap((rulesyncSubagent) => {
47407
+ const agentFile = VibeSubagent.fromRulesyncSubagent({
47408
+ outputRoot,
47409
+ relativeDirPath: VibeSubagent.getSettablePaths({ global }).relativeDirPath,
47410
+ rulesyncSubagent,
47411
+ global
47412
+ });
47413
+ const promptBody = rulesyncSubagent.getBody();
47414
+ if (!promptBody) return [agentFile];
47415
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
47416
+ return [agentFile, new VibeSubagent({
47417
+ outputRoot,
47418
+ body: promptBody,
47419
+ relativeDirPath: VIBE_PROMPTS_DIR_PATH,
47420
+ relativeFilePath: `${slug}.md`,
47421
+ fileContent: promptBody,
47422
+ promptFile: true,
47423
+ validate: false,
47424
+ global
47425
+ })];
47426
+ });
47427
+ }
46634
47428
  validate() {
46635
47429
  try {
46636
47430
  VibeSubagentTomlSchema.parse(smol_toml.parse(this.body));
@@ -46655,6 +47449,10 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46655
47449
  const paths = this.getSettablePaths({ global });
46656
47450
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
46657
47451
  const fileContent = await readFileContent(filePath);
47452
+ const resolvedSystemPrompt = await VibeSubagent.readSystemPrompt({
47453
+ outputRoot,
47454
+ fileContent
47455
+ });
46658
47456
  const subagent = new VibeSubagent({
46659
47457
  outputRoot,
46660
47458
  relativeDirPath: paths.relativeDirPath,
@@ -46662,7 +47460,8 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46662
47460
  body: fileContent.trim(),
46663
47461
  fileContent,
46664
47462
  validate,
46665
- global
47463
+ global,
47464
+ ...resolvedSystemPrompt !== void 0 && { resolvedSystemPrompt }
46666
47465
  });
46667
47466
  if (validate) {
46668
47467
  const result = subagent.validate();
@@ -46670,6 +47469,21 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46670
47469
  }
46671
47470
  return subagent;
46672
47471
  }
47472
+ /**
47473
+ * Read `.vibe/prompts/<id>.md` for an agent that names a `system_prompt_id`.
47474
+ * Returns `undefined` when there is no id or the file is missing, in which
47475
+ * case import falls back to the legacy `system_prompt` key.
47476
+ */
47477
+ static async readSystemPrompt({ outputRoot, fileContent }) {
47478
+ let promptId;
47479
+ try {
47480
+ promptId = VibeSubagentTomlSchema.parse(smol_toml.parse(fileContent)).system_prompt_id;
47481
+ } catch {
47482
+ return;
47483
+ }
47484
+ if (typeof promptId !== "string" || promptId === "" || /[\\/]/.test(promptId)) return;
47485
+ return await readFileContentOrNull((0, node_path.join)(outputRoot, VIBE_PROMPTS_DIR_PATH, `${promptId}.md`)) ?? void 0;
47486
+ }
46673
47487
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46674
47488
  return new VibeSubagent({
46675
47489
  outputRoot,
@@ -46712,7 +47526,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46712
47526
  const mode = super.toRooMode(rulesyncSubagent);
46713
47527
  const frontmatter = rulesyncSubagent.getFrontmatter();
46714
47528
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
46715
- if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
47529
+ if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
46716
47530
  return mode;
46717
47531
  }
46718
47532
  toRulesyncSubagents() {
@@ -46724,7 +47538,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46724
47538
  targets: ["zoocode"],
46725
47539
  roo: restRooSection
46726
47540
  };
46727
- if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
47541
+ if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
46728
47542
  return new RulesyncSubagent({
46729
47543
  outputRoot: ".",
46730
47544
  frontmatter: rebuilt,
@@ -49627,8 +50441,13 @@ const STRATEGIES = [
49627
50441
  * file per rule under `.devin/rules/*.md` (Devin Desktop Cascade) with YAML
49628
50442
  * frontmatter carrying a `trigger` (always_on | glob | manual | model_decision)
49629
50443
  * plus companion `globs`/`description` fields.
49630
- * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
49631
- * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
50444
+ * - Global scope: mirrors the project layout. The root rule is a single
50445
+ * plain-markdown, always-on file (no frontmatter) at
50446
+ * `~/.config/devin/AGENTS.md`; non-root rules are one file per rule under
50447
+ * `~/.devin/rules/*.md` with the same `trigger`/`globs` frontmatter as project
50448
+ * scope. Note the directory split — the per-rule global directory is the home
50449
+ * `~/.devin/`, not the `~/.config/devin/` tree every other Devin global
50450
+ * surface uses. That is what the rules page documents.
49632
50451
  *
49633
50452
  * Trigger inference (when no explicit devin trigger is persisted):
49634
50453
  * - Specific globs (non wildcard) → glob
@@ -49659,19 +50478,32 @@ var DevinRule = class DevinRule extends ToolRule {
49659
50478
  relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
49660
50479
  };
49661
50480
  }
50481
+ /**
50482
+ * The always-on root rule file for a scope: the project-root `AGENTS.md`
50483
+ * Devin CLI / Devin Local reads, or `~/.config/devin/AGENTS.md` in global
50484
+ * scope. Single-sourced so `getSettablePaths`, `fromFile` and
50485
+ * `fromRulesyncRule` cannot disagree about it.
50486
+ */
50487
+ static getRootPath(global, excludeToolDir) {
50488
+ return global ? DevinRule.getGlobalRootPath(excludeToolDir) : {
50489
+ relativeDirPath: ".",
50490
+ relativeFilePath: "AGENTS.md"
50491
+ };
50492
+ }
49662
50493
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
49663
- if (global) return { root: DevinRule.getGlobalRootPath(excludeToolDir) };
50494
+ if (global) return {
50495
+ root: DevinRule.getRootPath(true, excludeToolDir),
50496
+ nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
50497
+ };
49664
50498
  return {
49665
- root: {
49666
- relativeDirPath: ".",
49667
- relativeFilePath: "AGENTS.md"
49668
- },
50499
+ root: DevinRule.getRootPath(false, excludeToolDir),
49669
50500
  nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
49670
50501
  };
49671
50502
  }
49672
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
49673
- if (global) {
49674
- const rootPath = DevinRule.getGlobalRootPath();
50503
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
50504
+ const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
50505
+ if (relativeDirPath !== void 0 ? relativeDirPath !== nonRootDirPath : relativeFilePath === DevinRule.getRootPath(global).relativeFilePath) {
50506
+ const rootPath = DevinRule.getRootPath(global);
49675
50507
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
49676
50508
  return new DevinRule({
49677
50509
  outputRoot,
@@ -49683,19 +50515,6 @@ var DevinRule = class DevinRule extends ToolRule {
49683
50515
  root: true
49684
50516
  });
49685
50517
  }
49686
- if (relativeFilePath === "AGENTS.md") {
49687
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeFilePath));
49688
- return new DevinRule({
49689
- outputRoot,
49690
- relativeDirPath: ".",
49691
- relativeFilePath: "AGENTS.md",
49692
- frontmatter: {},
49693
- body: fileContent,
49694
- validate,
49695
- root: true
49696
- });
49697
- }
49698
- const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
49699
50518
  const filePath = (0, node_path.join)(outputRoot, nonRootDirPath, relativeFilePath);
49700
50519
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
49701
50520
  let parsedFrontmatter;
@@ -49715,8 +50534,9 @@ var DevinRule = class DevinRule extends ToolRule {
49715
50534
  });
49716
50535
  }
49717
50536
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
49718
- if (global) {
49719
- const rootPath = DevinRule.getGlobalRootPath();
50537
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
50538
+ if (rulesyncFrontmatter.root) {
50539
+ const rootPath = DevinRule.getRootPath(global);
49720
50540
  return new DevinRule({
49721
50541
  outputRoot,
49722
50542
  relativeDirPath: rootPath.relativeDirPath,
@@ -49727,16 +50547,6 @@ var DevinRule = class DevinRule extends ToolRule {
49727
50547
  root: true
49728
50548
  });
49729
50549
  }
49730
- const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
49731
- if (rulesyncFrontmatter.root) return new DevinRule({
49732
- outputRoot,
49733
- relativeDirPath: ".",
49734
- relativeFilePath: "AGENTS.md",
49735
- frontmatter: {},
49736
- body: rulesyncRule.getBody(),
49737
- validate,
49738
- root: true
49739
- });
49740
50550
  const storedDevin = rulesyncFrontmatter.devin;
49741
50551
  const normalized = normalizeStoredDevin(storedDevin);
49742
50552
  const storedTrigger = storedDevin?.trigger;
@@ -49796,7 +50606,7 @@ var DevinRule = class DevinRule extends ToolRule {
49796
50606
  error: null
49797
50607
  };
49798
50608
  }
49799
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50609
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
49800
50610
  return new DevinRule({
49801
50611
  outputRoot,
49802
50612
  relativeDirPath,
@@ -49804,7 +50614,7 @@ var DevinRule = class DevinRule extends ToolRule {
49804
50614
  frontmatter: {},
49805
50615
  body: "",
49806
50616
  validate: false,
49807
- root: global
50617
+ root: relativeDirPath !== buildToolPath(DEVIN_DIR, "rules")
49808
50618
  });
49809
50619
  }
49810
50620
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -50437,6 +51247,11 @@ var KimiCodeRule = class KimiCodeRule extends ToolRule {
50437
51247
  };
50438
51248
  //#endregion
50439
51249
  //#region src/features/rules/kiro-rule.ts
51250
+ /**
51251
+ * Kiro loads `AGENTS.md` as steering context from any directory in the
51252
+ * workspace tree (CLI 2.18.0 / IDE 1.0.309), not just the workspace root.
51253
+ */
51254
+ const KIRO_NESTED_STEERING_FILE_NAME = "AGENTS.md";
50440
51255
  const WILDCARD_GLOBS = /* @__PURE__ */ new Set([
50441
51256
  "**/*",
50442
51257
  "**",
@@ -50510,8 +51325,17 @@ var KiroRule = class KiroRule extends ToolRule {
50510
51325
  };
50511
51326
  return { nonRoot: { relativeDirPath: steeringDirPath } };
50512
51327
  }
50513
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
51328
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
50514
51329
  const paths = this.getSettablePaths({ global });
51330
+ const steeringDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
51331
+ if (overrideDirPath !== void 0 && overrideDirPath !== steeringDirPath && overrideDirPath !== "." && relativeFilePath === KIRO_NESTED_STEERING_FILE_NAME) return new KiroRule({
51332
+ outputRoot,
51333
+ relativeDirPath: overrideDirPath,
51334
+ relativeFilePath,
51335
+ fileContent: await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, relativeFilePath)),
51336
+ validate,
51337
+ root: false
51338
+ });
50515
51339
  const isRoot = "root" in paths && relativeFilePath === paths.root.relativeFilePath;
50516
51340
  const relativeDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
50517
51341
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
@@ -50535,6 +51359,12 @@ var KiroRule = class KiroRule extends ToolRule {
50535
51359
  });
50536
51360
  if (params.root) return new KiroRule(params);
50537
51361
  const frontmatter = rulesyncRule.getFrontmatter();
51362
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
51363
+ if (!global && subprojectPath) return new KiroRule({
51364
+ ...params,
51365
+ relativeDirPath: (0, node_path.join)(subprojectPath),
51366
+ relativeFilePath: KIRO_NESTED_STEERING_FILE_NAME
51367
+ });
50538
51368
  const inclusion = deriveKiroInclusion({
50539
51369
  kiro: frontmatter.kiro,
50540
51370
  globs: frontmatter.globs
@@ -50545,7 +51375,34 @@ var KiroRule = class KiroRule extends ToolRule {
50545
51375
  fileContent: stringifyFrontmatter(rulesyncRule.getBody(), inclusion)
50546
51376
  });
50547
51377
  }
51378
+ /**
51379
+ * The subproject directory a nested `AGENTS.md` scopes, or `undefined` for the
51380
+ * root rule and for the `.kiro/steering/` files.
51381
+ */
51382
+ getSubprojectPath() {
51383
+ if (this.isRoot() || this.getRelativeFilePath() !== KIRO_NESTED_STEERING_FILE_NAME) return;
51384
+ const relativeDirPath = toPosixPath(this.getRelativeDirPath());
51385
+ if (relativeDirPath === "." || relativeDirPath === "" || relativeDirPath.startsWith(".")) return;
51386
+ return relativeDirPath;
51387
+ }
50548
51388
  toRulesyncRule() {
51389
+ const subprojectPath = this.getSubprojectPath();
51390
+ if (subprojectPath !== void 0) {
51391
+ const slug = subprojectPath.replaceAll("/", "-");
51392
+ return new RulesyncRule({
51393
+ outputRoot: this.getOutputRoot(),
51394
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
51395
+ relativeFilePath: `${slug}-kiro.md`,
51396
+ frontmatter: {
51397
+ targets: ["kiro"],
51398
+ root: false,
51399
+ globs: [`${subprojectPath}/**/*`],
51400
+ agentsmd: { subprojectPath }
51401
+ },
51402
+ body: this.getFileContent(),
51403
+ validate: true
51404
+ });
51405
+ }
50549
51406
  const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getRelativeFilePath());
50550
51407
  const inclusion = typeof frontmatter.inclusion === "string" ? frontmatter.inclusion : void 0;
50551
51408
  if (!inclusion) return this.toRulesyncRuleDefault();
@@ -50578,6 +51435,26 @@ var KiroRule = class KiroRule extends ToolRule {
50578
51435
  error: null
50579
51436
  };
50580
51437
  }
51438
+ /**
51439
+ * Nested `AGENTS.md` files anywhere in the workspace tree, which Kiro loads as
51440
+ * steering context alongside `.kiro/steering/`. Import-only, like every other
51441
+ * nested scan: these are hand-authored files outside a rulesync-owned
51442
+ * directory, so enumerating them for `--delete` would sweep away work rulesync
51443
+ * never wrote.
51444
+ * @see https://kiro.dev/docs/steering/
51445
+ */
51446
+ static getNestedFilePatterns({ outputRoot }) {
51447
+ const root = toPosixPath(outputRoot);
51448
+ return {
51449
+ include: [`${root}/**/${KIRO_NESTED_STEERING_FILE_NAME}`],
51450
+ ignore: [
51451
+ `${root}/${KIRO_NESTED_STEERING_FILE_NAME}`,
51452
+ `${root}/**/.*/**`,
51453
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
51454
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
51455
+ ]
51456
+ };
51457
+ }
50581
51458
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50582
51459
  return new KiroRule({
50583
51460
  outputRoot,
@@ -51428,11 +52305,12 @@ var RooRule = class RooRule extends ToolRule {
51428
52305
  static getSettablePaths(_options = {}) {
51429
52306
  return { nonRoot: { relativeDirPath: buildToolPath(ROO_DIR, "rules", _options.excludeToolDir) } };
51430
52307
  }
51431
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
51432
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().nonRoot.relativeDirPath, relativeFilePath));
52308
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true }) {
52309
+ const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : this.getSettablePaths().nonRoot.relativeDirPath;
52310
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
51433
52311
  return new RooRule({
51434
52312
  outputRoot,
51435
- relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
52313
+ relativeDirPath,
51436
52314
  relativeFilePath,
51437
52315
  fileContent,
51438
52316
  validate,
@@ -51440,12 +52318,16 @@ var RooRule = class RooRule extends ToolRule {
51440
52318
  });
51441
52319
  }
51442
52320
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
51443
- return new RooRule(this.buildToolRuleParamsDefault({
52321
+ const params = this.buildToolRuleParamsDefault({
51444
52322
  outputRoot,
51445
52323
  rulesyncRule,
51446
52324
  validate,
51447
52325
  nonRootPath: this.getSettablePaths().nonRoot
51448
- }));
52326
+ });
52327
+ const mode = rulesyncRule.getFrontmatter().roo?.mode;
52328
+ if (!params.root && mode !== void 0 && mode !== "") if (!ROO_MODE_SLUG_PATTERN.test(mode)) warnWithFallback(void 0, `Ignoring roo.mode "${mode}" on ${rulesyncRule.getRelativeFilePath()}: a mode slug may contain only letters, digits and hyphens. Writing the rule to ${params.relativeDirPath} instead.`);
52329
+ else params.relativeDirPath = (0, node_path.join)((0, node_path.dirname)(params.relativeDirPath), rooModeRulesDirName(mode));
52330
+ return new RooRule(params);
51449
52331
  }
51450
52332
  /**
51451
52333
  * Extract mode slug from file path for mode-specific rules
@@ -51457,8 +52339,51 @@ var RooRule = class RooRule extends ToolRule {
51457
52339
  const singleFileMatch = filePath.match(/\.(roo|cline)rules-([a-zA-Z0-9-]+)$/);
51458
52340
  if (singleFileMatch) return singleFileMatch[2];
51459
52341
  }
52342
+ /**
52343
+ * The mode slug of a `.roo/rules-{mode}/` directory, or `undefined` for the
52344
+ * generic `.roo/rules/` directory and anything else. Path-shaped input is
52345
+ * rejected by the slug pattern, so a crafted directory name cannot be lifted
52346
+ * back into frontmatter.
52347
+ */
52348
+ static extractModeFromDirPath(relativeDirPath) {
52349
+ for (const segment of toPosixPath(relativeDirPath).split("/")) {
52350
+ if (!segment.startsWith("rules-")) continue;
52351
+ const mode = segment.slice(6);
52352
+ if (ROO_MODE_SLUG_PATTERN.test(mode)) return mode;
52353
+ }
52354
+ }
51460
52355
  toRulesyncRule() {
51461
- return this.toRulesyncRuleDefault();
52356
+ const mode = RooRule.extractModeFromDirPath(this.getRelativeDirPath());
52357
+ if (mode === void 0) return this.toRulesyncRuleDefault();
52358
+ const baseName = this.getRelativeFilePath().replace(/\.md$/, "");
52359
+ const suffix = `-${mode}`;
52360
+ const importedName = baseName.endsWith(suffix) ? baseName : `${baseName}${suffix}`;
52361
+ return new RulesyncRule({
52362
+ outputRoot: this.getOutputRoot(),
52363
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
52364
+ relativeFilePath: `${importedName}.md`,
52365
+ frontmatter: {
52366
+ root: false,
52367
+ targets: [this.constructor.getToolTargetName()],
52368
+ description: this.description,
52369
+ globs: this.globs ?? [],
52370
+ roo: { mode }
52371
+ },
52372
+ body: this.getFileContent(),
52373
+ validate: true
52374
+ });
52375
+ }
52376
+ /**
52377
+ * Mode-specific rule directories (`.roo/rules-{mode}/`), which Roo/Zoo Code
52378
+ * load instead of `.roo/rules/` while that mode is active. Import-only: the
52379
+ * generic directory is the only one the deletion sweep enumerates, because a
52380
+ * `rules-*` glob would also match mode rules a user wrote by hand.
52381
+ */
52382
+ static getNestedFilePatterns({ outputRoot }) {
52383
+ return {
52384
+ include: [`${toPosixPath(outputRoot)}/${toPosixPath(ROO_DIR)}/rules-*/**/*.md`],
52385
+ ignore: []
52386
+ };
51462
52387
  }
51463
52388
  validate() {
51464
52389
  return {
@@ -51479,10 +52404,18 @@ var RooRule = class RooRule extends ToolRule {
51479
52404
  static isTargetedByRulesyncRule(rulesyncRule) {
51480
52405
  return this.isTargetedByRulesyncRuleDefault({
51481
52406
  rulesyncRule,
51482
- toolTarget: "roo"
52407
+ toolTarget: this.getToolTargetName()
51483
52408
  });
51484
52409
  }
51485
52410
  /**
52411
+ * The tool target this class imports as. `ZoocodeRule` narrows the same
52412
+ * `.roo/` adapters to the `zoocode` target, so the name has to come from the
52413
+ * class rather than a literal.
52414
+ */
52415
+ static getToolTargetName() {
52416
+ return "roo";
52417
+ }
52418
+ /**
51486
52419
  * Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
51487
52420
  * at the project root, not under `.roo/` (mirrors rovodev).
51488
52421
  */
@@ -52015,11 +52948,8 @@ var ZedRule = class ZedRule extends ToolRule {
52015
52948
  * @see https://docs.zoocode.dev
52016
52949
  */
52017
52950
  var ZoocodeRule = class extends RooRule {
52018
- static isTargetedByRulesyncRule(rulesyncRule) {
52019
- return this.isTargetedByRulesyncRuleDefault({
52020
- rulesyncRule,
52021
- toolTarget: "zoocode"
52022
- });
52951
+ static getToolTargetName() {
52952
+ return "zoocode";
52023
52953
  }
52024
52954
  };
52025
52955
  //#endregion
@@ -52046,6 +52976,18 @@ const resolveIncludeLocalRoot = (options) => {
52046
52976
  return parsed.data.includeLocalRoot ?? true;
52047
52977
  };
52048
52978
  /**
52979
+ * Whether `candidate` is `base` itself or a class that extends it.
52980
+ *
52981
+ * Class-identity dispatch (`factory.class === Base`) is subclass-blind: a target
52982
+ * whose adapter merely narrows another one — `ZoocodeRule extends RooRule` — fails
52983
+ * a strict `===` and silently falls through. That is how `zoocode` ended up
52984
+ * declaring `localRootMode: "separate-local-file"` while never emitting
52985
+ * `AGENTS.local.md`, in both directions and without a warning.
52986
+ */
52987
+ function isClassOrSubclassOf({ candidate, base }) {
52988
+ return candidate === base || Object.prototype.isPrototypeOf.call(base, candidate);
52989
+ }
52990
+ /**
52049
52991
  * Factory Map mapping tool targets to their rule factories.
52050
52992
  * Using Map to preserve insertion order for consistent iteration.
52051
52993
  */
@@ -52399,7 +53341,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
52399
53341
  meta: {
52400
53342
  extension: "md",
52401
53343
  supportsGlobal: true,
52402
- ruleDiscoveryMode: "auto"
53344
+ ruleDiscoveryMode: "auto",
53345
+ localRootMode: "separate-local-file",
53346
+ localRootFileName: "AGENTS.local.md"
52403
53347
  }
52404
53348
  }],
52405
53349
  ["zed", {
@@ -52686,7 +53630,10 @@ var RulesProcessor = class extends FeatureProcessor {
52686
53630
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
52687
53631
  }
52688
53632
  buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
52689
- if (factory.class === ClaudecodeRule) {
53633
+ if (isClassOrSubclassOf({
53634
+ candidate: factory.class,
53635
+ base: ClaudecodeRule
53636
+ })) {
52690
53637
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
52691
53638
  return new ClaudecodeRule({
52692
53639
  outputRoot: this.outputRoot,
@@ -52699,7 +53646,10 @@ var RulesProcessor = class extends FeatureProcessor {
52699
53646
  localRoot
52700
53647
  });
52701
53648
  }
52702
- if (factory.class === ClaudecodeLegacyRule) {
53649
+ if (isClassOrSubclassOf({
53650
+ candidate: factory.class,
53651
+ base: ClaudecodeLegacyRule
53652
+ })) {
52703
53653
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
52704
53654
  return new ClaudecodeLegacyRule({
52705
53655
  outputRoot: this.outputRoot,
@@ -52711,7 +53661,10 @@ var RulesProcessor = class extends FeatureProcessor {
52711
53661
  localRoot
52712
53662
  });
52713
53663
  }
52714
- if (factory.class === RovodevRule) return new RovodevRule({
53664
+ if (isClassOrSubclassOf({
53665
+ candidate: factory.class,
53666
+ base: RovodevRule
53667
+ })) return new RovodevRule({
52715
53668
  outputRoot: this.outputRoot,
52716
53669
  relativeDirPath: relativeDirPath ?? ".",
52717
53670
  relativeFilePath: fileName,
@@ -52720,7 +53673,10 @@ var RulesProcessor = class extends FeatureProcessor {
52720
53673
  root: true,
52721
53674
  localRoot
52722
53675
  });
52723
- if (factory.class === RooRule) return new RooRule({
53676
+ if (isClassOrSubclassOf({
53677
+ candidate: factory.class,
53678
+ base: RooRule
53679
+ })) return new RooRule({
52724
53680
  outputRoot: this.outputRoot,
52725
53681
  relativeDirPath: relativeDirPath ?? ".",
52726
53682
  relativeFilePath: fileName,
@@ -52729,7 +53685,23 @@ var RulesProcessor = class extends FeatureProcessor {
52729
53685
  root: true,
52730
53686
  localRoot
52731
53687
  });
52732
- if (factory.class === QwencodeRule) return new QwencodeRule({
53688
+ if (isClassOrSubclassOf({
53689
+ candidate: factory.class,
53690
+ base: DevinRule
53691
+ })) return new DevinRule({
53692
+ outputRoot: this.outputRoot,
53693
+ relativeDirPath: relativeDirPath ?? ".",
53694
+ relativeFilePath: fileName,
53695
+ frontmatter: {},
53696
+ body,
53697
+ validate: true,
53698
+ root: true,
53699
+ localRoot
53700
+ });
53701
+ if (isClassOrSubclassOf({
53702
+ candidate: factory.class,
53703
+ base: QwencodeRule
53704
+ })) return new QwencodeRule({
52733
53705
  outputRoot: this.outputRoot,
52734
53706
  relativeDirPath: relativeDirPath ?? ".qwen",
52735
53707
  relativeFilePath: fileName,