rulesync 16.11.0 → 16.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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$289 } = 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$289(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$289(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$289(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$289(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$289(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$289(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$289(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$289(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
+ join$289(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$289(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$289(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$289(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$289(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$289(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,6 +437,7 @@ const permissionsProcessorToolTargetTuple = [
437
437
  "kiro-cli",
438
438
  "kiro-ide",
439
439
  "opencode",
440
+ "pi",
440
441
  "qwencode",
441
442
  "reasonix",
442
443
  "rovodev",
@@ -891,6 +892,7 @@ const ErrorCodes = {
891
892
  INIT_FAILED: "INIT_FAILED",
892
893
  MCP_FAILED: "MCP_FAILED",
893
894
  DOCTOR_FAILED: "DOCTOR_FAILED",
895
+ RELEASE_NOTES_FAILED: "RELEASE_NOTES_FAILED",
894
896
  UNKNOWN_ERROR: "UNKNOWN_ERROR"
895
897
  };
896
898
  /**
@@ -1927,7 +1929,7 @@ function isPlainObject$1(value) {
1927
1929
  /**
1928
1930
  * Type guard to check if a value is an array of strings.
1929
1931
  */
1930
- function isStringArray$1(value) {
1932
+ function isStringArray$2(value) {
1931
1933
  return Array.isArray(value) && value.every((item) => typeof item === "string");
1932
1934
  }
1933
1935
  //#endregion
@@ -2295,6 +2297,7 @@ const HOOK_EVENTS = [
2295
2297
  "todoCreated",
2296
2298
  "todoCompleted",
2297
2299
  "stopFailure",
2300
+ "stopCancelled",
2298
2301
  "instructionsLoaded",
2299
2302
  "userPromptExpansion",
2300
2303
  "postToolBatch",
@@ -2477,34 +2480,52 @@ const AMP_HOOK_EVENTS = [
2477
2480
  ];
2478
2481
  /**
2479
2482
  * Hook events supported by Cline's file-based hooks. Cline resolves one
2480
- * executable per lifecycle event from its hooks directory, and the event names
2481
- * it accepts are fixed by `VALID_HOOK_TYPES` in
2482
- * `apps/vscode/src/core/hooks/utils.ts`: `TaskStart`, `TaskResume`,
2483
- * `TaskCancel`, `TaskComplete`, `PreToolUse`, `PostToolUse`,
2484
- * `UserPromptSubmit`, `Notification` and `PreCompact`.
2483
+ * executable per lifecycle event from its hooks directory, and the accepted
2484
+ * event names come from two runtimes that read the same directory:
2485
+ *
2486
+ * - The VS Code extension fixes them in `VALID_HOOK_TYPES`
2487
+ * (`apps/vscode/src/core/hooks/utils.ts`): `TaskStart`, `TaskResume`,
2488
+ * `TaskCancel`, `TaskComplete`, `PreToolUse`, `PostToolUse`,
2489
+ * `UserPromptSubmit`, `Notification` and `PreCompact`.
2490
+ * - The SDK/CLI fixes them in `HookConfigFileName`
2491
+ * (`sdk/packages/core/src/hooks/hook-file-config.ts`), which drops
2492
+ * `Notification` but adds `TaskError` (→ `agent_error`) and
2493
+ * `SessionShutdown` (→ `session_shutdown`).
2494
+ *
2495
+ * This set is the union, because `.clinerules/hooks` is in both runtimes'
2496
+ * search paths and a script named for an event the running one does not know
2497
+ * is simply never spawned. That holds for unknown *names* only: for an event a
2498
+ * runtime does know, the SDK/CLI spawns both the extensionless script and its
2499
+ * `.ps1` twin, so each generated script opens with a guard that stands down on
2500
+ * the platform the other one owns — see `generateClineHookScript` and
2501
+ * `generateClineHookPowerShellScript`.
2485
2502
  *
2486
2503
  * `TaskResume` and `TaskCancel` have no canonical counterpart and stay
2487
2504
  * unmapped rather than being approximated by `sessionEnd` / `stop`, whose
2488
2505
  * semantics differ.
2489
2506
  *
2490
2507
  * @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
2508
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/hooks/hook-file-config.ts
2491
2509
  */
2492
2510
  const CLINE_HOOK_EVENTS = [
2493
2511
  "sessionStart",
2512
+ "sessionEnd",
2494
2513
  "preToolUse",
2495
2514
  "postToolUse",
2496
2515
  "beforeSubmitPrompt",
2497
2516
  "preCompact",
2498
2517
  "notification",
2499
- "taskCompleted"
2518
+ "taskCompleted",
2519
+ "afterError"
2500
2520
  ];
2501
2521
  /**
2502
2522
  * Hook events supported by GitHub Copilot (cloud coding agent).
2503
2523
  *
2504
2524
  * The events rulesync writes to `.github/hooks/*.json`:
2505
2525
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted` ← `beforeSubmitPrompt`,
2506
- * `preToolUse`, `postToolUse`, `agentStop` ← `stop`, `subagentStart`,
2507
- * `subagentStop`, `errorOccurred` ← `afterError`, and `preCompact`.
2526
+ * `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` ← `stop`,
2527
+ * `subagentStart`, `subagentStop`, `errorOccurred` ← `afterError`,
2528
+ * `preCompact`, and `userPromptTransformed` ← `userPromptExpansion`.
2508
2529
  *
2509
2530
  * `preCompact` and `subagentStart` are authorable because the unified hooks
2510
2531
  * reference's per-event "Cloud agent" column says both fire there. That column
@@ -2513,9 +2534,11 @@ const CLINE_HOOK_EVENTS = [
2513
2534
  * undo that. `notification` and `permissionRequest` stay out because the same
2514
2535
  * column is explicit that they do not fire on the cloud agent.
2515
2536
  *
2516
- * Two further CLI events — `postToolUseFailure` and `userPromptTransformed`
2517
- * ({@link COPILOTCLI_HOOK_EVENTS}) are documented as firing on the cloud
2518
- * agent as well but are not modelled here yet.
2537
+ * `postToolUseFailure` and `userPromptTransformed` are shared with
2538
+ * {@link COPILOTCLI_HOOK_EVENTS}; the same column records both as firing on the
2539
+ * cloud agent, so they are authorable here too. The event surfaces overlap but
2540
+ * the config surfaces do not: `copilot` emits `command` hooks only, while the
2541
+ * CLI adapter also handles `http` and `prompt`.
2519
2542
  *
2520
2543
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
2521
2544
  */
@@ -2525,11 +2548,13 @@ const COPILOT_HOOK_EVENTS = [
2525
2548
  "beforeSubmitPrompt",
2526
2549
  "preToolUse",
2527
2550
  "postToolUse",
2551
+ "postToolUseFailure",
2528
2552
  "stop",
2529
2553
  "subagentStart",
2530
2554
  "subagentStop",
2531
2555
  "afterError",
2532
- "preCompact"
2556
+ "preCompact",
2557
+ "userPromptExpansion"
2533
2558
  ];
2534
2559
  /**
2535
2560
  * Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
@@ -2848,6 +2873,7 @@ const GROKCLI_HOOK_EVENTS = [
2848
2873
  "permissionDenied",
2849
2874
  "stop",
2850
2875
  "stopFailure",
2876
+ "stopCancelled",
2851
2877
  "notification",
2852
2878
  "subagentStart",
2853
2879
  "subagentStop",
@@ -2923,16 +2949,10 @@ const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CAN
2923
2949
  /**
2924
2950
  * Hook events supported by Hermes Agent's native Shell Hooks system.
2925
2951
  *
2926
- * Hermes validates hook events against a fixed `VALID_HOOKS` set:
2927
- * `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
2928
- * `pre_verify`, `pre_api_request`, `post_api_request`, `api_request_error`,
2929
- * `on_session_start`, `on_session_end`, `on_session_finalize`,
2930
- * `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
2931
- * `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
2932
- * `transform_terminal_output`, `transform_llm_output`, and the three
2933
- * `kanban_task_*` events. Only the events with a clean 1:1 canonical equivalent
2934
- * are mapped here. All other native events round-trip through
2935
- * `hermesagent.hooks`.
2952
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
2953
+ * of v0.20.2 (`v2026.8.16`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
2954
+ * full list. Only the events with a clean 1:1 canonical equivalent are mapped
2955
+ * here. All other native events round-trip through `hermesagent.hooks`.
2936
2956
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
2937
2957
  */
2938
2958
  const HERMESAGENT_HOOK_EVENTS = [
@@ -2945,6 +2965,21 @@ const HERMESAGENT_HOOK_EVENTS = [
2945
2965
  "subagentStart",
2946
2966
  "subagentStop"
2947
2967
  ];
2968
+ /**
2969
+ * Hermes Agent's native `VALID_HOOKS` set, in upstream source order.
2970
+ *
2971
+ * The authority is `hermes_cli/plugins.py`'s `VALID_HOOKS`, not the docs: at
2972
+ * v0.20.2 `website/docs/user-guide/features/plugins.md` says "26 lifecycle
2973
+ * events" and then lists 30, omitting the five `on_kanban_*` worker/task/dispatch
2974
+ * observers, `gateway_platform_event` and `transform_api_error_classification`.
2975
+ * Shell hooks are gated on the same set.
2976
+ *
2977
+ * Verified against the tag `v2026.8.16` (v0.20.2): `VALID_HOOKS` holds 37
2978
+ * entries, grown from 23 at `v2026.8.3` (v0.20.0). This list holds 36 of them —
2979
+ * see {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
2980
+ *
2981
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py
2982
+ */
2948
2983
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2949
2984
  "pre_tool_call",
2950
2985
  "post_tool_call",
@@ -2953,6 +2988,10 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2953
2988
  "transform_llm_output",
2954
2989
  "pre_llm_call",
2955
2990
  "post_llm_call",
2991
+ "on_stream_start",
2992
+ "on_stream_delta",
2993
+ "on_stream_end",
2994
+ "on_interim_message",
2956
2995
  "pre_verify",
2957
2996
  "pre_api_request",
2958
2997
  "post_api_request",
@@ -2961,16 +3000,39 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2961
3000
  "on_session_end",
2962
3001
  "on_session_finalize",
2963
3002
  "on_session_reset",
3003
+ "on_skill_lifecycle",
2964
3004
  "subagent_start",
2965
3005
  "subagent_stop",
2966
3006
  "pre_gateway_dispatch",
2967
3007
  "pre_approval_request",
2968
3008
  "post_approval_response",
3009
+ "pre_transcription",
2969
3010
  "kanban_task_claimed",
2970
3011
  "kanban_task_completed",
2971
- "kanban_task_blocked"
3012
+ "kanban_task_blocked",
3013
+ "on_kanban_worker_spawned",
3014
+ "on_kanban_worker_exited",
3015
+ "on_kanban_worker_stale_claim",
3016
+ "on_kanban_task_updated",
3017
+ "on_kanban_dispatch_tick",
3018
+ "gateway_platform_event",
3019
+ "pre_command"
2972
3020
  ];
2973
3021
  /**
3022
+ * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
3023
+ * as the shell-hook allow-list, but `SHELL_UNSUPPORTED_HOOKS` is subtracted from
3024
+ * it first: shell hooks cannot return these events' directives, so
3025
+ * `_parse_hooks_block` refuses the registration with a warning rather than
3026
+ * accepting it and dropping the return value.
3027
+ *
3028
+ * rulesync only ever writes shell hooks, so these are excluded from
3029
+ * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
3030
+ *
3031
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
3032
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/agent/shell_hooks.py — `_parse_hooks_block`
3033
+ */
3034
+ const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
3035
+ /**
2974
3036
  * Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
2975
3037
  * snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
2976
3038
  */
@@ -3245,15 +3307,21 @@ const CANONICAL_TO_AMP_EVENT_NAMES = {
3245
3307
  beforeSubmitPrompt: "agent.start",
3246
3308
  stop: "agent.end"
3247
3309
  };
3248
- /** Map canonical hook events to Cline's `VALID_HOOK_TYPES` file names. */
3310
+ /**
3311
+ * Map canonical hook events to Cline's hook script file names — the union of
3312
+ * the VS Code extension's `VALID_HOOK_TYPES` and the SDK/CLI's
3313
+ * `HookConfigFileName`, see {@link CLINE_HOOK_EVENTS}.
3314
+ */
3249
3315
  const CANONICAL_TO_CLINE_EVENT_NAMES = {
3250
3316
  sessionStart: "TaskStart",
3317
+ sessionEnd: "SessionShutdown",
3251
3318
  preToolUse: "PreToolUse",
3252
3319
  postToolUse: "PostToolUse",
3253
3320
  beforeSubmitPrompt: "UserPromptSubmit",
3254
3321
  preCompact: "PreCompact",
3255
3322
  notification: "Notification",
3256
- taskCompleted: "TaskComplete"
3323
+ taskCompleted: "TaskComplete",
3324
+ afterError: "TaskError"
3257
3325
  };
3258
3326
  /**
3259
3327
  * Map canonical camelCase event names to Copilot camelCase.
@@ -3264,11 +3332,13 @@ const CANONICAL_TO_COPILOT_EVENT_NAMES = {
3264
3332
  beforeSubmitPrompt: "userPromptSubmitted",
3265
3333
  preToolUse: "preToolUse",
3266
3334
  postToolUse: "postToolUse",
3335
+ postToolUseFailure: "postToolUseFailure",
3267
3336
  stop: "agentStop",
3268
3337
  subagentStart: "subagentStart",
3269
3338
  subagentStop: "subagentStop",
3270
3339
  afterError: "errorOccurred",
3271
- preCompact: "preCompact"
3340
+ preCompact: "preCompact",
3341
+ userPromptExpansion: "userPromptTransformed"
3272
3342
  };
3273
3343
  /**
3274
3344
  * Map Copilot camelCase event names to canonical camelCase.
@@ -3579,6 +3649,7 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
3579
3649
  permissionDenied: "PermissionDenied",
3580
3650
  stop: "Stop",
3581
3651
  stopFailure: "StopFailure",
3652
+ stopCancelled: "StopCancelled",
3582
3653
  notification: "Notification",
3583
3654
  subagentStart: "SubagentStart",
3584
3655
  subagentStop: "SubagentStop",
@@ -3853,6 +3924,25 @@ const McpServerSchema = zod_mini.z.looseObject({
3853
3924
  kiroAutoApprove: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
3854
3925
  kiroAutoBlock: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
3855
3926
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
3927
+ /**
3928
+ * The canonical per-server tool allowlist.
3929
+ *
3930
+ * **Collision rule for adapters.** Several tools spell an allowlist natively
3931
+ * under a name that also exists here, so a single server entry can carry both
3932
+ * a canonical field and its native counterpart. What the *native* key means
3933
+ * decides the resolution — not which one was written first:
3934
+ *
3935
+ * - Native key is a redundant spelling of the same concept, and both lists are
3936
+ * additive → **merge** them, so a config that already spells the field
3937
+ * natively keeps working (`kiroAutoApprove`/`kiroAutoBlock` in
3938
+ * `kiro-mcp.ts`).
3939
+ * - Native key is the same concept but rulesync owns the generated value, so
3940
+ * keeping both would be ambiguous → the **canonical value wins** and the
3941
+ * native one is dropped with a warning (`tools` in `copilotcli-mcp.ts`).
3942
+ * - Native key of the same name means something else entirely → **refuse the
3943
+ * canonical value** rather than write a shape the tool misreads (`tools` in
3944
+ * `codexcli-mcp.ts`, where Codex reads it as a per-tool approval table).
3945
+ */
3856
3946
  enabledTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
3857
3947
  disabledTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
3858
3948
  });
@@ -4695,8 +4785,65 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4695
4785
  dangerouslyAllowAll: zod_mini.z.optional(zod_mini.z.boolean())
4696
4786
  });
4697
4787
  /**
4788
+ * Tool-scoped override block for Pi Coding Agent. Pi has no allow/ask/deny rule
4789
+ * surface: the only repository-syncable tool gate is `defaultTools`, the list of
4790
+ * built-in tools enabled at startup (added in v0.84.2). It is an enable-list
4791
+ * rather than a canonical allow/deny rule set, so it is authored here rather
4792
+ * than mapped onto `permission`, mirroring `KimiCodePermissionsOverrideSchema`'s
4793
+ * `tools.enabled` and `VibePermissionsOverrideSchema`'s `enabled_tools`.
4794
+ *
4795
+ * An empty array is meaningful upstream — it starts Pi with no built-in tools
4796
+ * while keeping extension and SDK custom tools — so it is emitted as written.
4797
+ * CLI flags (`--tools`, `--no-tools`, `--no-builtin-tools`, `--exclude-tools`)
4798
+ * outrank the setting, and a project array **replaces** the global one rather
4799
+ * than merging with it.
4800
+ *
4801
+ * @example
4802
+ * { "defaultTools": ["bash", "edit", "write"] }
4803
+ * @see https://pi.dev/docs/latest/settings
4804
+ */
4805
+ const PiPermissionsOverrideSchema = zod_mini.z.looseObject({
4806
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4807
+ defaultTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
4808
+ });
4809
+ /**
4810
+ * The Antigravity CLI's four documented `toolPermission` autonomy presets.
4811
+ * Exported so the import side of `antigravity-cli-permissions.ts` filters
4812
+ * against exactly the values the enum below accepts — an unrecognized value
4813
+ * carried in verbatim would fail validation of the *whole*
4814
+ * `.rulesync/permissions.jsonc`, not just that key.
4815
+ * @see https://antigravity.google/docs/cli/reference
4816
+ */
4817
+ const ANTIGRAVITY_CLI_TOOL_PERMISSIONS = [
4818
+ "request-review",
4819
+ "proceed-in-sandbox",
4820
+ "always-proceed",
4821
+ "strict"
4822
+ ];
4823
+ /**
4824
+ * The Antigravity CLI's three documented `artifactReviewPolicy` values. Shared
4825
+ * with the import filter for the same reason as
4826
+ * {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4827
+ * @see https://antigravity.google/docs/cli/settings
4828
+ */
4829
+ const ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES = [
4830
+ "asks-for-review",
4831
+ "agent-decides",
4832
+ "always-proceed"
4833
+ ];
4834
+ /**
4835
+ * The baseline execution modes the CLI persists as `agentMode`. Shared with the
4836
+ * import filter for the same reason as {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4837
+ * @see https://antigravity.google/docs/cli/modes
4838
+ */
4839
+ const ANTIGRAVITY_CLI_AGENT_MODES = [
4840
+ "default",
4841
+ "accept-edits",
4842
+ "plan"
4843
+ ];
4844
+ /**
4698
4845
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4699
- * `settings.json` carries four global autonomy/sandbox knobs outside the
4846
+ * `settings.json` carries five global autonomy/sandbox knobs outside the
4700
4847
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4701
4848
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4702
4849
  * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
@@ -4704,7 +4851,8 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4704
4851
  * agent's artifact changes are gated on a review prompt — `asks-for-review`
4705
4852
  * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4706
4853
  * (a boolean, off by default, letting the agent read or write files outside the
4707
- * active workspace roots). Antigravity applies the allow/deny
4854
+ * active workspace roots), and `agentMode` (the baseline execution mode a session
4855
+ * starts in — `default` / `accept-edits` / `plan`). Antigravity applies the allow/deny
4708
4856
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4709
4857
  * authors these keys verbatim — no precedence modeling is needed on our side.
4710
4858
  * Fields placed here are merged onto the top level of
@@ -4712,27 +4860,27 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4712
4860
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4713
4861
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4714
4862
  * Verified against https://antigravity.google/docs/cli/reference,
4715
- * https://antigravity.google/docs/cli/sandbox and
4716
- * https://antigravity.google/docs/cli/settings.
4863
+ * https://antigravity.google/docs/cli/sandbox,
4864
+ * https://antigravity.google/docs/cli/settings and
4865
+ * https://antigravity.google/docs/cli/modes.
4717
4866
  *
4718
4867
  * @example
4719
4868
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
4720
4869
  */
4721
4870
  const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
4722
4871
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4723
- toolPermission: zod_mini.z.optional(zod_mini.z.enum([
4724
- "request-review",
4725
- "proceed-in-sandbox",
4726
- "always-proceed",
4727
- "strict"
4728
- ])),
4872
+ toolPermission: zod_mini.z.optional(zod_mini.z.enum(ANTIGRAVITY_CLI_TOOL_PERMISSIONS)),
4729
4873
  enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean()),
4730
- artifactReviewPolicy: zod_mini.z.optional(zod_mini.z.enum([
4731
- "asks-for-review",
4732
- "agent-decides",
4733
- "always-proceed"
4734
- ])),
4735
- allowNonWorkspaceAccess: zod_mini.z.optional(zod_mini.z.boolean())
4874
+ artifactReviewPolicy: zod_mini.z.optional(zod_mini.z.enum(ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES)),
4875
+ allowNonWorkspaceAccess: zod_mini.z.optional(zod_mini.z.boolean()),
4876
+ /**
4877
+ * The baseline execution mode the CLI starts a session in (Agent Mode), the
4878
+ * persisted form of the `/settings` panel's Agent Mode row. Only
4879
+ * `~/.gemini/antigravity-cli/settings.json` is read for it, which is why this
4880
+ * override stays global-only.
4881
+ * @see https://antigravity.google/docs/cli/modes
4882
+ */
4883
+ agentMode: zod_mini.z.optional(zod_mini.z.enum(ANTIGRAVITY_CLI_AGENT_MODES))
4736
4884
  });
4737
4885
  /**
4738
4886
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -5042,6 +5190,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
5042
5190
  takt: zod_mini.z.optional(TaktPermissionsOverrideSchema),
5043
5191
  amp: zod_mini.z.optional(AmpPermissionsOverrideSchema),
5044
5192
  "antigravity-cli": zod_mini.z.optional(AntigravityCliPermissionsOverrideSchema),
5193
+ pi: zod_mini.z.optional(PiPermissionsOverrideSchema),
5045
5194
  augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
5046
5195
  kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
5047
5196
  codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
@@ -5493,6 +5642,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
5493
5642
  })),
5494
5643
  cline: zod_mini.z.optional(zod_mini.z.looseObject({})),
5495
5644
  roo: zod_mini.z.optional(zod_mini.z.looseObject({})),
5645
+ amp: zod_mini.z.optional(zod_mini.z.looseObject({})),
5496
5646
  devin: zod_mini.z.optional(zod_mini.z.looseObject({
5497
5647
  "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
5498
5648
  model: zod_mini.z.optional(zod_mini.z.string()),
@@ -5522,6 +5672,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
5522
5672
  cursor: zod_mini.z.optional(zod_mini.z.looseObject({
5523
5673
  paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
5524
5674
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
5675
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
5525
5676
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
5526
5677
  })),
5527
5678
  factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -5746,6 +5897,11 @@ async function getLocalSkillDirNames(outputRoot) {
5746
5897
  * default with a per-target value. A defined section value (including `false`)
5747
5898
  * always wins over the root default.
5748
5899
  *
5900
+ * `devin` also consumes the root-level value — it maps `true` onto a user-only
5901
+ * `triggers` list (see `devin-skill.ts`) — but has no section key of the same
5902
+ * name, so it does not go through this helper. A `devin.triggers` section value
5903
+ * still overrides it.
5904
+ *
5749
5905
  * @returns The resolved boolean, or `undefined` when neither value is set.
5750
5906
  */
5751
5907
  function resolveDisableModelInvocation({ rootFrontmatter, section }) {
@@ -5756,10 +5912,15 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
5756
5912
  *
5757
5913
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
5758
5914
  * that applies to every tool supporting the flag (claudecode, copilot,
5759
- * copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5915
+ * copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5760
5916
  * per-target value. A defined section value (including `false`) always wins
5761
5917
  * over the root default.
5762
5918
  *
5919
+ * `devin` also consumes the root-level value — it maps `false` onto a
5920
+ * model-only `triggers` list (see `devin-skill.ts`) — but has no section key of
5921
+ * the same name, so it does not go through this helper. A `devin.triggers`
5922
+ * section value still overrides it.
5923
+ *
5763
5924
  * @returns The resolved boolean, or `undefined` when neither value is set.
5764
5925
  */
5765
5926
  function resolveUserInvocable({ rootFrontmatter, section }) {
@@ -7609,6 +7770,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7609
7770
  }
7610
7771
  }
7611
7772
  },
7773
+ ".pi/settings.json": {
7774
+ format: "json",
7775
+ invalidRootPolicy: "error",
7776
+ features: { permissions: {
7777
+ kind: "replace-owned-keys",
7778
+ ownedKeys: ["defaultTools"]
7779
+ } }
7780
+ },
7781
+ ".pi/agent/settings.json": {
7782
+ format: "json",
7783
+ invalidRootPolicy: "error",
7784
+ features: { permissions: {
7785
+ kind: "replace-owned-keys",
7786
+ ownedKeys: ["defaultTools"]
7787
+ } }
7788
+ },
7612
7789
  ".github/copilot/settings.json": {
7613
7790
  format: "json",
7614
7791
  invalidRootPolicy: "error",
@@ -11725,7 +11902,8 @@ const KiloCommandFrontmatterSchema = zod_mini.z.looseObject({
11725
11902
  description: zod_mini.z.optional(zod_mini.z.string()),
11726
11903
  agent: zod_mini.z.optional(zod_mini.z.string()),
11727
11904
  subtask: zod_mini.z.optional(zod_mini.z.boolean()),
11728
- model: zod_mini.z.optional(zod_mini.z.string())
11905
+ model: zod_mini.z.optional(zod_mini.z.string()),
11906
+ variant: zod_mini.z.optional(zod_mini.z.string())
11729
11907
  });
11730
11908
  var KiloCommand = class KiloCommand extends ToolCommand {
11731
11909
  frontmatter;
@@ -12194,6 +12372,8 @@ const PI_RULE_FILE_NAME = "AGENTS.md";
12194
12372
  const PI_RULE_OVERRIDE_FILE_NAME = "AGENTS.override.md";
12195
12373
  const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
12196
12374
  const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
12375
+ const PI_SETTINGS_FILE_NAME = "settings.json";
12376
+ const PI_AGENT_DIR_PATH = PI_AGENT_DIR;
12197
12377
  //#endregion
12198
12378
  //#region src/features/commands/pi-command.ts
12199
12379
  /**
@@ -14118,7 +14298,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
14118
14298
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
14119
14299
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
14120
14300
  const isEmittableString = ({ value }) => isNonEmptyString(value);
14121
- const isEmittableArray = ({ value }) => isStringArray(value);
14301
+ const isEmittableArray = ({ value }) => isStringArray$1(value);
14122
14302
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
14123
14303
  const isImportableArray = ({ value }) => isSafeStringArray(value);
14124
14304
  /**
@@ -14427,7 +14607,7 @@ function isStringRecord(value) {
14427
14607
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
14428
14608
  return Object.values(value).every((v) => typeof v === "string");
14429
14609
  }
14430
- function isStringArray(value) {
14610
+ function isStringArray$1(value) {
14431
14611
  return Array.isArray(value) && value.every((v) => typeof v === "string");
14432
14612
  }
14433
14613
  /** Compare object values without letting key order decide the answer. */
@@ -14459,7 +14639,7 @@ function isSafeStringRecord(value) {
14459
14639
  * skipped wholesale when that read fails.
14460
14640
  */
14461
14641
  function isSafeStringArray(value) {
14462
- return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14642
+ return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14463
14643
  }
14464
14644
  /**
14465
14645
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -14501,7 +14681,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
14501
14681
  return {
14502
14682
  ...url !== void 0 && { url },
14503
14683
  ...isStringRecord(headers) && { headers },
14504
- ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14684
+ ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14505
14685
  };
14506
14686
  }
14507
14687
  if (hookType === "mcp_tool") {
@@ -15407,6 +15587,13 @@ function generateClineHookScript({ event, commands }) {
15407
15587
  `# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
15408
15588
  `# ${CLINE_HOOK_SCRIPT_MARKER}`,
15409
15589
  "",
15590
+ "case \"${OSTYPE:-$(uname -s 2>/dev/null || true)}\" in",
15591
+ " msys*|MSYS*|cygwin*|CYGWIN*|MINGW*|mingw*)",
15592
+ ` printf '{"cancel": false, "contextModification": "", "errorMessage": ""}\\n'`,
15593
+ " exit 0",
15594
+ " ;;",
15595
+ "esac",
15596
+ "",
15410
15597
  "payload=$(cat)",
15411
15598
  "cancel=false",
15412
15599
  "error_message=''",
@@ -15420,15 +15607,39 @@ function generateClineHookScript({ event, commands }) {
15420
15607
  return lines.join("\n");
15421
15608
  }
15422
15609
  /**
15423
- * The PowerShell twin of {@link generateClineHookScript}. On Windows Cline
15424
- * resolves only `<Event>.ps1` and runs it through `powershell -File`, so both
15425
- * spellings are written and the platform picks one.
15610
+ * The PowerShell twin of {@link generateClineHookScript}. On Windows the VS Code
15611
+ * extension resolves only `<Event>.ps1` and runs it through `powershell -File`,
15612
+ * so both spellings are written and the platform picks one.
15613
+ *
15614
+ * The SDK/CLI runtime does not pick one. `listHookConfigFiles` dedupes by path,
15615
+ * so `TaskError` and `TaskError.ps1` are two entries naming the same event, and
15616
+ * `createHookCommandMap` appends both to that event's command list without a
15617
+ * per-event dedupe — it then runs every command in the list, spawning a `.ps1`
15618
+ * through `pwsh` on Unix too. That produced noise rather than a second
15619
+ * execution (the body shells out through `cmd /c`, which Unix has no such
15620
+ * thing), but it is still a failure reported on every fire.
15621
+ *
15622
+ * Hence the leading platform guard: off Windows the script answers with the
15623
+ * neutral success payload and exits, leaving the POSIX twin to do the work.
15624
+ * Its counterpart at the top of {@link generateClineHookScript} covers the
15625
+ * quadrant where the duplication is real — Windows with a POSIX shell.
15626
+ * `$IsWindows` only exists in PowerShell 6+, and is `$null` under the Windows
15627
+ * PowerShell 5.1 that `powershell -File` starts — so the guard tests that it is
15628
+ * defined *and* false, rather than `-not $IsWindows`, which would be true on
15629
+ * 5.1 and would no-op the script on the one platform it exists for.
15630
+ *
15631
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/hooks/hook-file-hooks.ts
15426
15632
  */
15427
15633
  function generateClineHookPowerShellScript({ event, commands }) {
15428
15634
  const lines = [
15429
15635
  `# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
15430
15636
  `# ${CLINE_HOOK_SCRIPT_MARKER}`,
15431
15637
  "",
15638
+ "if ($null -ne $IsWindows -and -not $IsWindows) {",
15639
+ ` Write-Output '{"cancel": false, "contextModification": "", "errorMessage": ""}'`,
15640
+ " exit 0",
15641
+ "}",
15642
+ "",
15432
15643
  "$payload = [Console]::In.ReadToEnd()",
15433
15644
  "$cancel = $false",
15434
15645
  "$errorMessage = ''",
@@ -15453,7 +15664,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
15453
15664
  function parseManifest(fileContent) {
15454
15665
  try {
15455
15666
  const parsed = JSON.parse(fileContent);
15456
- if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
15667
+ if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
15457
15668
  return {
15458
15669
  generatedBy: "rulesync",
15459
15670
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -15490,10 +15701,14 @@ var ClineHookScript = class extends ToolFile {
15490
15701
  * by the contract, so every generated script carries a marker line and a script
15491
15702
  * without it is never overwritten.
15492
15703
  *
15493
- * Cline's CLI and SDK use a different, in-process hook surface (`AgentHooks`
15494
- * from `@cline/core`), which this adapter does not target.
15704
+ * The project hooks directory is in the search paths of both the VS Code
15705
+ * extension and the SDK/CLI, whose accepted event names differ slightly, so the
15706
+ * emitted set is their union ({@link CANONICAL_TO_CLINE_EVENT_NAMES}). Cline's
15707
+ * in-process hook surface (`AgentHooks` from `@cline/core`) is a separate
15708
+ * mechanism this adapter does not target.
15495
15709
  *
15496
15710
  * @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
15711
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/hooks/hook-file-config.ts
15497
15712
  */
15498
15713
  var ClineHooks = class ClineHooks extends ToolHooks {
15499
15714
  scriptsByEvent;
@@ -15648,6 +15863,11 @@ const CODEXCLI_CONVERTER_CONFIG = {
15648
15863
  numberPassthroughFields: [{
15649
15864
  canonical: "additionalContextLimit",
15650
15865
  tool: "additionalContextLimit"
15866
+ }],
15867
+ booleanPassthroughFields: [{
15868
+ canonical: "async",
15869
+ tool: "async",
15870
+ commandOnly: true
15651
15871
  }]
15652
15872
  };
15653
15873
  /**
@@ -16774,6 +16994,16 @@ const FACTORYDROID_CONVERTER_CONFIG = {
16774
16994
  subdividesGroup: true
16775
16995
  }]
16776
16996
  };
16997
+ /** Droid's nine event names, the keys a standalone `hooks.json` is made of. */
16998
+ const FACTORYDROID_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_FACTORYDROID_EVENT_NAMES));
16999
+ /**
17000
+ * Whether a parsed hooks file is the standalone shape — keyed directly by event
17001
+ * name — rather than the `settings.json` shape that wraps the same map in a
17002
+ * `hooks` key.
17003
+ */
17004
+ function hasFactorydroidEventKey(parsed) {
17005
+ return Object.keys(parsed).some((key) => FACTORYDROID_EVENT_NAMES.has(key));
17006
+ }
16777
17007
  var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16778
17008
  constructor(params) {
16779
17009
  super({
@@ -16801,14 +17031,6 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16801
17031
  }
16802
17032
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
16803
17033
  const paths = FactorydroidHooks.getSettablePaths({ global });
16804
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16805
- const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
16806
- let settings;
16807
- try {
16808
- settings = JSON.parse(existingContent);
16809
- } catch (error) {
16810
- throw new Error(`Failed to parse existing Factory Droid hooks file at ${filePath}: ${formatError(error)}`, { cause: error });
16811
- }
16812
17034
  const config = rulesyncHooks.getJson();
16813
17035
  const factorydroidHooks = canonicalToToolHooks({
16814
17036
  config,
@@ -16816,11 +17038,7 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16816
17038
  converterConfig: FACTORYDROID_CONVERTER_CONFIG,
16817
17039
  logger
16818
17040
  });
16819
- const merged = {
16820
- ...settings,
16821
- hooks: factorydroidHooks
16822
- };
16823
- const fileContent = JSON.stringify(merged, null, 2);
17041
+ const fileContent = JSON.stringify(factorydroidHooks, null, 2);
16824
17042
  return new FactorydroidHooks({
16825
17043
  outputRoot,
16826
17044
  relativeDirPath: paths.relativeDirPath,
@@ -16830,14 +17048,14 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16830
17048
  });
16831
17049
  }
16832
17050
  toRulesyncHooks({ logger } = {}) {
16833
- let settings;
17051
+ let parsed;
16834
17052
  try {
16835
- settings = JSON.parse(this.getFileContent());
17053
+ parsed = JSON.parse(this.getFileContent());
16836
17054
  } catch (error) {
16837
17055
  throw new Error(`Failed to parse Factory Droid hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
16838
17056
  }
16839
17057
  const hooks = toolHooksToCanonical({
16840
- hooks: settings.hooks,
17058
+ hooks: hasFactorydroidEventKey(parsed) ? parsed : parsed.hooks,
16841
17059
  converterConfig: FACTORYDROID_CONVERTER_CONFIG,
16842
17060
  logger
16843
17061
  });
@@ -16972,18 +17190,7 @@ const GROKCLI_CONVERTER_CONFIG = {
16972
17190
  tool: "env",
16973
17191
  commandOnly: true
16974
17192
  }],
16975
- noMatcherEvents: /* @__PURE__ */ new Set([
16976
- "sessionStart",
16977
- "sessionEnd",
16978
- "beforeSubmitPrompt",
16979
- "stop",
16980
- "stopFailure",
16981
- "notification",
16982
- "subagentStart",
16983
- "subagentStop",
16984
- "preCompact",
16985
- "postCompact"
16986
- ])
17193
+ noMatcherEvents: /* @__PURE__ */ new Set(["stop", "beforeSubmitPrompt"])
16987
17194
  };
16988
17195
  /**
16989
17196
  * Hooks generator for Grok CLI (xAI Grok Build).
@@ -17092,6 +17299,7 @@ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "po
17092
17299
  const HERMESAGENT_FAIL_CLOSED_EVENT = "pre_tool_call";
17093
17300
  const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
17094
17301
  const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
17302
+ const HERMESAGENT_SHELL_UNSUPPORTED_EVENTS = new Set(HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS);
17095
17303
  /**
17096
17304
  * Whether an entry of the `hooks:` mapping is a hook-event list rather than one
17097
17305
  * of its non-event siblings.
@@ -17180,7 +17388,8 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
17180
17388
  }
17181
17389
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
17182
17390
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
17183
- 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.`);
17391
+ 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.`);
17392
+ 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.`);
17184
17393
  setHermesHookEntries({
17185
17394
  result,
17186
17395
  event: nativeEvent,
@@ -22304,7 +22513,7 @@ function isCodexStdioServer(config) {
22304
22513
  * @see https://learn.chatgpt.com/docs/extend/mcp
22305
22514
  */
22306
22515
  function isValidRenamedArray(key, value) {
22307
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
22516
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
22308
22517
  }
22309
22518
  /**
22310
22519
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
@@ -22824,6 +23033,49 @@ const resolveCopilotcliServerType = (server) => {
22824
23033
  return isRemoteMcpServer(server) ? "http" : "stdio";
22825
23034
  };
22826
23035
  /**
23036
+ * Copilot CLI's per-server tool allowlist. `tools` selects which of the
23037
+ * server's tools are exposed — `["*"]` (the default) for all, or a list of
23038
+ * names — so the canonical `enabledTools` is written under that name rather
23039
+ * than passed through as a key the CLI ignores.
23040
+ *
23041
+ * When a server carries both, the canonical `enabledTools` wins and the native
23042
+ * `tools` is dropped with a warning. rulesync owns the generated allowlist, and
23043
+ * the `tools` value most likely present is the upstream default `["*"]` — left
23044
+ * to win, it would silently discard a real allowlist in favour of "expose every
23045
+ * tool", which is the failure this mapping exists to prevent. That is the
23046
+ * "canonical wins" branch of the collision rule documented on `enabledTools` in
23047
+ * `src/types/mcp.ts`; `kiro-mcp.ts` merges instead (its native names are
23048
+ * redundant spellings of additive lists) and `codexcli-mcp.ts` refuses instead
23049
+ * (Codex's same-named key means a different thing).
23050
+ *
23051
+ * `disabledTools` has no counterpart upstream (expressing it would need the
23052
+ * server's full tool list), so the target keeps `supportsDisabledTools: false`
23053
+ * and the processor strips it before it reaches here.
23054
+ *
23055
+ * @see https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
23056
+ */
23057
+ function renameEnabledToolsToTools({ server, serverName, logger }) {
23058
+ const { enabledTools, ...rest } = server;
23059
+ if (enabledTools === void 0) return rest;
23060
+ if (rest.tools !== void 0) logger?.warn(`[CopilotcliMcp] MCP server "${serverName}" declares both 'tools' and 'enabledTools'; keeping 'enabledTools' and dropping 'tools'.`);
23061
+ return {
23062
+ ...rest,
23063
+ tools: enabledTools
23064
+ };
23065
+ }
23066
+ /**
23067
+ * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
23068
+ * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
23069
+ * CLI-written entry can be a bare string. Canonical `enabledTools` is
23070
+ * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
23071
+ * through would reject the WHOLE imported MCP file rather than one key. Anything
23072
+ * that is not a string array is therefore left under `tools` untouched, matching
23073
+ * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
23074
+ */
23075
+ function isStringArray(value) {
23076
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
23077
+ }
23078
+ /**
22827
23079
  * Resolves and sets the transport type for each MCP server config.
22828
23080
  * GitHub Copilot CLI requires the "type" field for each server. A server it
22829
23081
  * cannot express — one that names no transport, or names one it carries no way
@@ -22867,7 +23119,11 @@ function addTypeField(mcpServers, logger) {
22867
23119
  }
22868
23120
  const { httpUrl: _httpUrl, ...rest } = parsed;
22869
23121
  result[name] = {
22870
- ...rest,
23122
+ ...renameEnabledToolsToTools({
23123
+ server: rest,
23124
+ serverName: name,
23125
+ logger
23126
+ }),
22871
23127
  type,
22872
23128
  url
22873
23129
  };
@@ -22885,7 +23141,11 @@ function addTypeField(mcpServers, logger) {
22885
23141
  continue;
22886
23142
  }
22887
23143
  result[name] = {
22888
- ...local,
23144
+ ...renameEnabledToolsToTools({
23145
+ server: local,
23146
+ serverName: name,
23147
+ logger
23148
+ }),
22889
23149
  type,
22890
23150
  command: head,
22891
23151
  ...tail.length > 0 && { args: tail }
@@ -22899,11 +23159,19 @@ function addTypeField(mcpServers, logger) {
22899
23159
  function removeTypeField(config) {
22900
23160
  const result = {};
22901
23161
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
22902
- if (server.type !== "stdio") {
22903
- result[name] = server;
23162
+ const { tools, ...withoutTools } = server;
23163
+ const restored = isStringArray(tools) ? {
23164
+ ...withoutTools,
23165
+ enabledTools: tools
23166
+ } : {
23167
+ ...withoutTools,
23168
+ ...tools !== void 0 && { tools }
23169
+ };
23170
+ if (restored.type !== "stdio") {
23171
+ result[name] = restored;
22904
23172
  continue;
22905
23173
  }
22906
- const { type: _, ...rest } = server;
23174
+ const { type: _, ...rest } = restored;
22907
23175
  result[name] = rest;
22908
23176
  }
22909
23177
  return result;
@@ -23164,6 +23432,78 @@ var CursorMcp = class CursorMcp extends ToolMcp {
23164
23432
  };
23165
23433
  //#endregion
23166
23434
  //#region src/features/mcp/deepagents-mcp.ts
23435
+ const TOOL_NAME = "deepagents";
23436
+ /**
23437
+ * Map a canonical transport onto the three dcode accepts.
23438
+ *
23439
+ * `_resolve_server_type` takes `stdio`, `sse` and `http`, plus the aliases
23440
+ * `streamable_http` / `streamable-http` → `http`. Rulesync's canonical
23441
+ * vocabulary is wider: `local` and `ws` are spellings dcode rejects outright,
23442
+ * and a rejected server is dropped at load time with only a log line. `local`
23443
+ * has an exact equivalent so it is translated; `ws` has none and is skipped at
23444
+ * generate time instead, where the warning can still reach the author.
23445
+ *
23446
+ * @see https://docs.langchain.com/oss/deepagents/code/mcp-tools
23447
+ */
23448
+ function normalizeDeepagentsTransport(transport) {
23449
+ switch (transport) {
23450
+ case "local":
23451
+ case "stdio": return "stdio";
23452
+ case "streamable-http":
23453
+ case "streamable_http":
23454
+ case "http": return "http";
23455
+ case "sse": return "sse";
23456
+ default: return;
23457
+ }
23458
+ }
23459
+ /**
23460
+ * Translate one canonical server into dcode's `.mcp.json` shape, or `null` to
23461
+ * skip it.
23462
+ *
23463
+ * Two upstream constraints from `_validate_tool_filter_fields` are enforced
23464
+ * here, because breaking either one makes dcode drop the whole server: the two
23465
+ * filters are mutually exclusive on a single server, and neither may be an
23466
+ * empty list.
23467
+ */
23468
+ function toDeepagentsServer({ name, server, logger }) {
23469
+ const rawTransport = server.transport ?? server.type;
23470
+ if (rawTransport === "ws") return warnAndSkipMcpServer({
23471
+ toolName: TOOL_NAME,
23472
+ serverName: name,
23473
+ reason: "the WebSocket transport, which deepagents does not support",
23474
+ logger
23475
+ });
23476
+ const { enabledTools, disabledTools, type: _type, transport, ...rest } = server;
23477
+ const converted = { ...rest };
23478
+ const normalized = normalizeDeepagentsTransport(rawTransport);
23479
+ if (normalized !== void 0) if (transport !== void 0) converted.transport = normalized;
23480
+ else converted.type = normalized;
23481
+ if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
23482
+ toolName: TOOL_NAME,
23483
+ serverName: name,
23484
+ reason: "both enabledTools and disabledTools, which deepagents rejects — pick one",
23485
+ logger
23486
+ });
23487
+ if (enabledTools !== void 0) {
23488
+ if (enabledTools.length === 0) return warnAndSkipMcpServer({
23489
+ toolName: TOOL_NAME,
23490
+ serverName: name,
23491
+ reason: "an empty enabledTools list, which allows no tools at all and which deepagents rejects",
23492
+ logger
23493
+ });
23494
+ converted.allowedTools = enabledTools;
23495
+ } else if (disabledTools !== void 0) if (disabledTools.length === 0) logger?.warn(`${TOOL_NAME} MCP: dropping the empty disabledTools list on "${name}"; it denies nothing, and deepagents rejects the empty form.`);
23496
+ else converted.disabledTools = disabledTools;
23497
+ return converted;
23498
+ }
23499
+ /** Lift dcode's spellings back into the canonical model. */
23500
+ function toRulesyncServer(server) {
23501
+ const { allowedTools, ...rest } = server;
23502
+ const converted = { ...rest };
23503
+ for (const key of ["type", "transport"]) if (converted[key] === "streamable_http" || converted[key] === "streamable-http") converted[key] = "http";
23504
+ if (Array.isArray(allowedTools)) converted.enabledTools = allowedTools;
23505
+ return converted;
23506
+ }
23167
23507
  var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
23168
23508
  json;
23169
23509
  constructor(params) {
@@ -23198,12 +23538,22 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
23198
23538
  validate
23199
23539
  });
23200
23540
  }
23201
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
23541
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
23202
23542
  const paths = this.getSettablePaths({ global });
23203
23543
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2);
23544
+ const json = JSON.parse(fileContent);
23545
+ const mcpServers = {};
23546
+ for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
23547
+ const converted = toDeepagentsServer({
23548
+ name,
23549
+ server,
23550
+ logger
23551
+ });
23552
+ if (converted !== null) mcpServers[name] = converted;
23553
+ }
23204
23554
  const mcpJson = {
23205
- ...JSON.parse(fileContent),
23206
- mcpServers: rulesyncMcp.getMcpServers()
23555
+ ...json,
23556
+ mcpServers
23207
23557
  };
23208
23558
  return new DeepagentsMcp({
23209
23559
  outputRoot,
@@ -23214,7 +23564,9 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
23214
23564
  });
23215
23565
  }
23216
23566
  toRulesyncMcp() {
23217
- return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: this.json.mcpServers }, null, 2) });
23567
+ const servers = isRecord$1(this.json.mcpServers) ? this.json.mcpServers : {};
23568
+ const mcpServers = Object.fromEntries(Object.entries(servers).map(([name, server]) => [name, isRecord$1(server) ? toRulesyncServer(server) : server]));
23569
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
23218
23570
  }
23219
23571
  validate() {
23220
23572
  return {
@@ -23471,11 +23823,11 @@ function applyGooseStdioFields(ext, config) {
23471
23823
  if (Array.isArray(command)) {
23472
23824
  if (typeof command[0] === "string") ext.cmd = command[0];
23473
23825
  const rest = command.slice(1).filter((c) => typeof c === "string");
23474
- const args = isStringArray$1(config.args) ? config.args : [];
23826
+ const args = isStringArray$2(config.args) ? config.args : [];
23475
23827
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
23476
23828
  } else if (typeof command === "string") {
23477
23829
  ext.cmd = command;
23478
- if (isStringArray$1(config.args)) ext.args = config.args;
23830
+ if (isStringArray$2(config.args)) ext.args = config.args;
23479
23831
  }
23480
23832
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
23481
23833
  }
@@ -23577,7 +23929,7 @@ function convertFromGooseFormat(extensions) {
23577
23929
  else if (type === "streamable_http") server.type = "http";
23578
23930
  else server.type = "stdio";
23579
23931
  if (typeof ext.cmd === "string") server.command = ext.cmd;
23580
- if (isStringArray$1(ext.args)) server.args = ext.args;
23932
+ if (isStringArray$2(ext.args)) server.args = ext.args;
23581
23933
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
23582
23934
  if (typeof ext.uri === "string") server.url = ext.uri;
23583
23935
  if (isPlainObject$1(ext.headers)) server.headers = omitPrototypePollutionKeys(ext.headers);
@@ -23598,11 +23950,11 @@ function buildGoosePluginStdioServer(config) {
23598
23950
  if (Array.isArray(command)) {
23599
23951
  if (typeof command[0] === "string") server.command = command[0];
23600
23952
  const rest = command.slice(1).filter((c) => typeof c === "string");
23601
- const args = isStringArray$1(config.args) ? config.args : [];
23953
+ const args = isStringArray$2(config.args) ? config.args : [];
23602
23954
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
23603
23955
  } else if (typeof command === "string") {
23604
23956
  server.command = command;
23605
- if (isStringArray$1(config.args)) server.args = config.args;
23957
+ if (isStringArray$2(config.args)) server.args = config.args;
23606
23958
  }
23607
23959
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
23608
23960
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -23934,7 +24286,7 @@ function copyHermesOauth(source) {
23934
24286
  "client_secret"
23935
24287
  ]) if (typeof source[key] === "string") oauth[key] = source[key];
23936
24288
  if (typeof source.redirect_port === "number") oauth.redirect_port = source.redirect_port;
23937
- if (isStringArray$1(source.scopes)) oauth.scopes = source.scopes;
24289
+ if (isStringArray$2(source.scopes)) oauth.scopes = source.scopes;
23938
24290
  return Object.keys(oauth).length > 0 ? oauth : void 0;
23939
24291
  }
23940
24292
  function copyHermesAdvancedFields(source, target) {
@@ -23943,7 +24295,7 @@ function copyHermesAdvancedFields(source, target) {
23943
24295
  target.auth = source.auth;
23944
24296
  copied = true;
23945
24297
  }
23946
- if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
24298
+ if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
23947
24299
  target.client_cert = source.client_cert;
23948
24300
  copied = true;
23949
24301
  }
@@ -24012,8 +24364,8 @@ function copyHermesAdvancedFields(source, target) {
24012
24364
  */
24013
24365
  function buildHermesToolsBlock(config) {
24014
24366
  const tools = {};
24015
- if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
24016
- if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
24367
+ if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
24368
+ if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
24017
24369
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
24018
24370
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
24019
24371
  return tools;
@@ -24025,8 +24377,8 @@ function buildHermesToolsBlock(config) {
24025
24377
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
24026
24378
  */
24027
24379
  function applyHermesToolsBlock(hermesTools, server) {
24028
- if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
24029
- if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24380
+ if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
24381
+ if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24030
24382
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
24031
24383
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
24032
24384
  }
@@ -24052,11 +24404,11 @@ function convertServerToHermes(config) {
24052
24404
  if (Array.isArray(command)) {
24053
24405
  if (typeof command[0] === "string") out.command = command[0];
24054
24406
  const rest = command.slice(1).filter((c) => typeof c === "string");
24055
- const args = isStringArray$1(config.args) ? config.args : [];
24407
+ const args = isStringArray$2(config.args) ? config.args : [];
24056
24408
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
24057
24409
  } else if (typeof command === "string") {
24058
24410
  out.command = command;
24059
- if (isStringArray$1(config.args)) out.args = config.args;
24411
+ if (isStringArray$2(config.args)) out.args = config.args;
24060
24412
  }
24061
24413
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
24062
24414
  } else if (url !== void 0) {
@@ -24106,7 +24458,7 @@ function convertFromHermesFormat(mcpServers) {
24106
24458
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
24107
24459
  const server = {};
24108
24460
  if (typeof config.command === "string") server.command = config.command;
24109
- if (isStringArray$1(config.args)) server.args = config.args;
24461
+ if (isStringArray$2(config.args)) server.args = config.args;
24110
24462
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
24111
24463
  if (typeof config.url === "string") server.url = config.url;
24112
24464
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -25064,13 +25416,17 @@ function mergeToolLists(...lists) {
25064
25416
  *
25065
25417
  * Both native names are documented per-server fields, so a config that already
25066
25418
  * spells them natively keeps working: the two lists are merged rather than
25067
- * one overwriting the other.
25419
+ * one overwriting the other. That is the "merge" branch of the collision rule
25420
+ * documented on `enabledTools` in `src/types/mcp.ts` — it applies here because
25421
+ * the native names are redundant spellings of the same additive lists;
25422
+ * `copilotcli-mcp.ts` lets the canonical value win instead, and
25423
+ * `codexcli-mcp.ts` refuses the canonical value instead.
25068
25424
  * @see https://kiro.dev/docs/mcp/configuration/
25069
25425
  */
25070
25426
  function toKiroMcpServers(servers) {
25071
25427
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25072
25428
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
25073
- const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25429
+ const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25074
25430
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
25075
25431
  return [name, {
25076
25432
  ...rest,
@@ -25094,7 +25450,7 @@ function fromKiroMcpServers(servers) {
25094
25450
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25095
25451
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
25096
25452
  const { autoApprove, ...rest } = server;
25097
- if (!isStringArray$1(autoApprove)) return [name, server];
25453
+ if (!isStringArray$2(autoApprove)) return [name, server];
25098
25454
  return [name, {
25099
25455
  ...rest,
25100
25456
  kiroAutoApprove: autoApprove
@@ -26236,7 +26592,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
26236
26592
  }
26237
26593
  function disabledNamesOf(config) {
26238
26594
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
26239
- return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26595
+ return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26240
26596
  }
26241
26597
  /**
26242
26598
  * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
@@ -26369,7 +26725,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
26369
26725
  return [];
26370
26726
  }
26371
26727
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
26372
- const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26728
+ const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26373
26729
  const managedNameSet = new Set(managedNames);
26374
26730
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
26375
26731
  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.`);
@@ -27211,7 +27567,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27211
27567
  meta: {
27212
27568
  supportsProject: true,
27213
27569
  supportsGlobal: true,
27214
- supportsEnabledTools: false,
27570
+ supportsEnabledTools: true,
27215
27571
  supportsDisabledTools: false
27216
27572
  }
27217
27573
  }],
@@ -27229,8 +27585,8 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27229
27585
  meta: {
27230
27586
  supportsProject: true,
27231
27587
  supportsGlobal: true,
27232
- supportsEnabledTools: false,
27233
- supportsDisabledTools: false
27588
+ supportsEnabledTools: true,
27589
+ supportsDisabledTools: true
27234
27590
  }
27235
27591
  }],
27236
27592
  ["factorydroid", {
@@ -27239,7 +27595,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27239
27595
  supportsProject: true,
27240
27596
  supportsGlobal: true,
27241
27597
  supportsEnabledTools: false,
27242
- supportsDisabledTools: false
27598
+ supportsDisabledTools: true
27243
27599
  }
27244
27600
  }],
27245
27601
  ["goose", {
@@ -27365,7 +27721,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27365
27721
  supportsProject: true,
27366
27722
  supportsGlobal: false,
27367
27723
  supportsEnabledTools: false,
27368
- supportsDisabledTools: false
27724
+ supportsDisabledTools: true
27369
27725
  }
27370
27726
  }],
27371
27727
  ["zoocode", {
@@ -27374,7 +27730,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27374
27730
  supportsProject: true,
27375
27731
  supportsGlobal: false,
27376
27732
  supportsEnabledTools: false,
27377
- supportsDisabledTools: false
27733
+ supportsDisabledTools: true
27378
27734
  }
27379
27735
  }],
27380
27736
  ["rovodev", {
@@ -28035,6 +28391,34 @@ function convertAmpToRulesync({ disable, permissions }) {
28035
28391
  //#endregion
28036
28392
  //#region src/features/permissions/antigravity-cli-permissions.ts
28037
28393
  /**
28394
+ * Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
28395
+ * permissions override may author. They are siblings of `permissions`, passed
28396
+ * through verbatim in both directions.
28397
+ */
28398
+ const ANTIGRAVITY_CLI_OVERRIDE_KEYS = [
28399
+ "toolPermission",
28400
+ "enableTerminalSandbox",
28401
+ "artifactReviewPolicy",
28402
+ "allowNonWorkspaceAccess",
28403
+ "agentMode"
28404
+ ];
28405
+ const moduleLogger$2 = fallbackLogger;
28406
+ /**
28407
+ * Narrow an imported `settings.json` value to one of the documented values of
28408
+ * an enum-typed override key. The override schema declares `toolPermission`,
28409
+ * `artifactReviewPolicy` and `agentMode` as `z.enum`, so carrying an
28410
+ * unrecognized value through — a hand-written typo, or a preset a newer
28411
+ * Antigravity release added — would fail validation of the whole imported
28412
+ * `.rulesync/permissions.jsonc` instead of dropping just that key. A dropped
28413
+ * value is warned about rather than discarded silently, so a preset rulesync
28414
+ * does not know about yet is visible instead of quietly missing.
28415
+ */
28416
+ function pickDocumentedValue({ key, value, documented }) {
28417
+ if (typeof value !== "string") return void 0;
28418
+ if (documented.includes(value)) return value;
28419
+ 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.`);
28420
+ }
28421
+ /**
28038
28422
  * Deliberate mapping from rulesync canonical permission categories to the
28039
28423
  * Antigravity CLI Fine-Grained Permissions Engine action vocabulary
28040
28424
  * (`read_file`, `write_file`, `read_url`, `command`, `mcp`, plus `execute_url` /
@@ -28109,9 +28493,10 @@ function buildPermissionEntry$1(toolName, pattern) {
28109
28493
  * file (global scope only). The file holds other CLI settings besides
28110
28494
  * permissions, so it is never deleted.
28111
28495
  *
28112
- * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28496
+ * Five CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28113
28497
  * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
28114
- * `artifactReviewPolicy` and `allowNonWorkspaceAccess` — are authored and
28498
+ * `artifactReviewPolicy`, `allowNonWorkspaceAccess` and `agentMode` (the
28499
+ * baseline execution mode) — are authored and
28115
28500
  * round-tripped through the `antigravity-cli` permissions override (see
28116
28501
  * `AntigravityCliPermissionsOverrideSchema`).
28117
28502
  */
@@ -28175,10 +28560,10 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28175
28560
  permissions: mergedPermissions
28176
28561
  };
28177
28562
  const override = config["antigravity-cli"];
28178
- if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
28179
- if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
28180
- if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
28181
- if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
28563
+ for (const key of ANTIGRAVITY_CLI_OVERRIDE_KEYS) {
28564
+ const value = override?.[key];
28565
+ if (value !== void 0) merged[key] = value;
28566
+ }
28182
28567
  const fileContent = JSON.stringify(merged, null, 2);
28183
28568
  return new AntigravityCliPermissions({
28184
28569
  outputRoot,
@@ -28203,10 +28588,26 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28203
28588
  deny: permissions.deny ?? []
28204
28589
  });
28205
28590
  const override = {};
28206
- if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
28591
+ const toolPermission = pickDocumentedValue({
28592
+ key: "toolPermission",
28593
+ value: settings.toolPermission,
28594
+ documented: ANTIGRAVITY_CLI_TOOL_PERMISSIONS
28595
+ });
28596
+ if (toolPermission !== void 0) override.toolPermission = toolPermission;
28207
28597
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
28208
- if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
28598
+ const artifactReviewPolicy = pickDocumentedValue({
28599
+ key: "artifactReviewPolicy",
28600
+ value: settings.artifactReviewPolicy,
28601
+ documented: ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES
28602
+ });
28603
+ if (artifactReviewPolicy !== void 0) override.artifactReviewPolicy = artifactReviewPolicy;
28209
28604
  if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
28605
+ const agentMode = pickDocumentedValue({
28606
+ key: "agentMode",
28607
+ value: settings.agentMode,
28608
+ documented: ANTIGRAVITY_CLI_AGENT_MODES
28609
+ });
28610
+ if (agentMode !== void 0) override.agentMode = agentMode;
28210
28611
  const result = { ...config };
28211
28612
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
28212
28613
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -29032,6 +29433,119 @@ function deepMergeRecords(base, patch) {
29032
29433
  }
29033
29434
  return merged;
29034
29435
  }
29436
+ /**
29437
+ * `sandbox.*` paths Claude Code honors only from user settings, managed settings
29438
+ * and the `--settings` CLI flag. Written into a project `.claude/settings.json`
29439
+ * they are silently ignored, so a committed file would read as though it
29440
+ * enforced a sandbox policy while doing nothing — security-relevant for
29441
+ * `network.strictAllowlist` and the credential-masking keys in particular.
29442
+ * Generation therefore drops them at project scope with a per-key warning and
29443
+ * emits them only under `--global`.
29444
+ *
29445
+ * Deliberately NOT listed:
29446
+ * - `bwrapPath` / `socatPath`: v2.1.232 added them to the managed-settings
29447
+ * approval dialog, which is a consent prompt, not a project-scope rejection.
29448
+ * - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
29449
+ * unit is the individual entry's mode, not the settings key, and the same
29450
+ * lists carry `deny` entries that project settings *do* honor — dropping a
29451
+ * whole list would remove real restrictions. `stripProjectIgnoredMaskEntries`
29452
+ * filters those lists per entry instead.
29453
+ *
29454
+ * @see https://code.claude.com/docs/en/sandboxing
29455
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
29456
+ */
29457
+ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29458
+ ["filesystem", "disabled"],
29459
+ ["network", "strictAllowlist"],
29460
+ ["network", "tlsTerminate"],
29461
+ ["credentials", "allowPlaintextInject"],
29462
+ ["credentials", "awsPairs"],
29463
+ ["credentials", "sigv4"],
29464
+ ["allowAppleEvents"],
29465
+ ["ripgrep"]
29466
+ ];
29467
+ /**
29468
+ * Copy of the authored `sandbox` override with the user/managed-only paths
29469
+ * removed, warning once per dropped path. Only the override copy is filtered —
29470
+ * a value already hand-written in the target file is left untouched, matching
29471
+ * the `qwencode` `security.allowPrivateNetworkHooks` precedent.
29472
+ */
29473
+ function stripGlobalOnlySandboxPaths({ sandbox, relativeFilePath, logger }) {
29474
+ const filtered = structuredClone(sandbox);
29475
+ for (const path of CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS) {
29476
+ const leaf = path.at(-1);
29477
+ if (leaf === void 0) continue;
29478
+ const parentPath = path.slice(0, -1);
29479
+ let parent = filtered;
29480
+ for (const segment of parentPath) {
29481
+ const next = parent[segment];
29482
+ if (!isPlainRecord(next)) {
29483
+ parent = {};
29484
+ break;
29485
+ }
29486
+ parent = next;
29487
+ }
29488
+ if (parent[leaf] === void 0) continue;
29489
+ delete parent[leaf];
29490
+ const [container] = parentPath;
29491
+ if (container !== void 0 && isPlainRecord(filtered[container])) {
29492
+ if (Object.keys(filtered[container]).length === 0) delete filtered[container];
29493
+ }
29494
+ 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.`);
29495
+ }
29496
+ return filtered;
29497
+ }
29498
+ /** The `sandbox.credentials` lists whose entries accept `"mode": "mask"`. */
29499
+ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
29500
+ /**
29501
+ * Copy of the authored `sandbox` override with the `credentials.envVars` /
29502
+ * `credentials.files` entries whose `mode` is `"mask"` removed, warning once per
29503
+ * list.
29504
+ *
29505
+ * Masking authorizes the sandbox proxy to send the real credential to the listed
29506
+ * hosts, so Claude Code honors it only from user settings, managed settings and
29507
+ * the `--settings` CLI flag; a `mask` entry in a repository's
29508
+ * `.claude/settings.json` is ignored outright. Keeping it would read as though
29509
+ * the credential were masked while nothing protects it — the one project-scope
29510
+ * gap whose consequence is a live credential leaving the sandbox unmasked.
29511
+ *
29512
+ * Filtered per entry rather than per key, because the same lists carry `deny`
29513
+ * entries that project settings *do* honor. `mode` is matched exactly: an entry
29514
+ * Claude Code cannot read as `mask` is not treated as one either (it degrades
29515
+ * such entries to `deny`), so the "reads as masked but isn't" state this guards
29516
+ * against cannot slip through a differently-spelled value.
29517
+ *
29518
+ * Like `stripGlobalOnlySandboxPaths`, only the override copy is filtered — a
29519
+ * value already in the target file is left untouched, which is why the warning
29520
+ * points at it.
29521
+ *
29522
+ * @see https://code.claude.com/docs/en/sandboxing — "`mask` entries … are all
29523
+ * ignored in a repository's `.claude/settings.json` or
29524
+ * `.claude/settings.local.json`"; the file-entry section states the same
29525
+ * settings-source restriction applies there.
29526
+ */
29527
+ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
29528
+ const credentials = sandbox.credentials;
29529
+ if (!isPlainRecord(credentials)) return sandbox;
29530
+ const filteredCredentials = { ...credentials };
29531
+ let changed = false;
29532
+ for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
29533
+ const list = filteredCredentials[listKey];
29534
+ if (!Array.isArray(list)) continue;
29535
+ const kept = list.filter((entry) => !(isPlainRecord(entry) && entry.mode === "mask"));
29536
+ if (kept.length === list.length) continue;
29537
+ changed = true;
29538
+ const dropped = list.length - kept.length;
29539
+ 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.`);
29540
+ if (kept.length === 0) delete filteredCredentials[listKey];
29541
+ else filteredCredentials[listKey] = kept;
29542
+ }
29543
+ if (!changed) return sandbox;
29544
+ const filtered = { ...sandbox };
29545
+ if (Object.keys(filteredCredentials).length === 0) delete filtered.credentials;
29546
+ else filtered.credentials = filteredCredentials;
29547
+ return filtered;
29548
+ }
29035
29549
  const CLAUDE_PATH_RULE_ALIASES = {
29036
29550
  Write: "Edit",
29037
29551
  NotebookEdit: "Edit",
@@ -29085,7 +29599,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29085
29599
  validate
29086
29600
  });
29087
29601
  }
29088
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
29602
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
29089
29603
  const paths = ClaudecodePermissions.getSettablePaths();
29090
29604
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
29091
29605
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -29109,7 +29623,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29109
29623
  };
29110
29624
  }
29111
29625
  const overrideSandbox = config.claudecode?.sandbox;
29112
- if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
29626
+ if (isPlainRecord(overrideSandbox)) {
29627
+ const scopedSandbox = global ? overrideSandbox : stripProjectIgnoredMaskEntries({
29628
+ sandbox: stripGlobalOnlySandboxPaths({
29629
+ sandbox: overrideSandbox,
29630
+ relativeFilePath: paths.relativeFilePath,
29631
+ logger
29632
+ }),
29633
+ relativeFilePath: paths.relativeFilePath,
29634
+ logger
29635
+ });
29636
+ if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
29637
+ }
29113
29638
  const managedToolNames = managedClaudeToolNames(config);
29114
29639
  const merged = applyPermissions({
29115
29640
  settings,
@@ -31286,7 +31811,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
31286
31811
  function convertGoosePermissionConfigToRulesync(userPermission) {
31287
31812
  const permission = {};
31288
31813
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
31289
- const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
31814
+ const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
31290
31815
  const action = GOOSE_LIST_TO_ACTION[key];
31291
31816
  for (const toolName of toolNames) {
31292
31817
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
@@ -31301,6 +31826,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
31301
31826
  const GROKCLI_UI_KEY = "ui";
31302
31827
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
31303
31828
  const GROKCLI_PERMISSION_KEY = "permission";
31829
+ const GROKCLI_AUTO_PERMISSION_MODE = "auto";
31304
31830
  const CATCH_ALL_PATTERN$2 = "*";
31305
31831
  const MCP_CANONICAL_PREFIX$1 = "mcp__";
31306
31832
  const CATEGORY_TO_GROK_TOOL = {
@@ -31514,8 +32040,9 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
31514
32040
  deny: buckets.deny,
31515
32041
  ask: buckets.ask
31516
32042
  };
31517
- const uiPatch = global ? { [GROKCLI_UI_KEY]: {
31518
- ...isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
32043
+ const existingUi = isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
32044
+ const uiPatch = global && existingUi[GROKCLI_PERMISSION_MODE_KEY] !== GROKCLI_AUTO_PERMISSION_MODE ? { [GROKCLI_UI_KEY]: {
32045
+ ...existingUi,
31519
32046
  [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
31520
32047
  } } : {};
31521
32048
  return new GrokcliPermissions({
@@ -31577,7 +32104,7 @@ const ACTION_RANK = {
31577
32104
  * (mirrors the Cursor adapter's preservation of unmanaged types).
31578
32105
  */
31579
32106
  function unmanagedEntries(existingPermission, key) {
31580
- return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
32107
+ return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
31581
32108
  }
31582
32109
  /**
31583
32110
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -31620,9 +32147,9 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
31620
32147
  * described more than once resolves to the strictest action.
31621
32148
  */
31622
32149
  function parseGrokPermissionArrays(permission) {
31623
- const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
31624
- const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
31625
- const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
32150
+ const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
32151
+ const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
32152
+ const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
31626
32153
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31627
32154
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
31628
32155
  const result = {};
@@ -31804,20 +32331,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
31804
32331
  const permission = clonePermissionBlock(provenance.permission);
31805
32332
  reconcileCommandAllowlist({
31806
32333
  permission,
31807
- commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
32334
+ commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
31808
32335
  });
31809
32336
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
31810
32337
  reconcileNativeDenies({
31811
32338
  permission,
31812
32339
  category: "bash",
31813
- patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
32340
+ patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
31814
32341
  });
31815
32342
  const security = isRecord$1(config.security) ? config.security : {};
31816
32343
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
31817
32344
  reconcileNativeDenies({
31818
32345
  permission,
31819
32346
  category: "webfetch",
31820
- patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
32347
+ patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
31821
32348
  });
31822
32349
  removeEmptyCategories(permission);
31823
32350
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -33245,6 +33772,115 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
33245
33772
  }
33246
33773
  };
33247
33774
  //#endregion
33775
+ //#region src/features/permissions/pi-permissions.ts
33776
+ /** The one `settings.json` key this feature owns. */
33777
+ const DEFAULT_TOOLS_KEY = "defaultTools";
33778
+ /**
33779
+ * Permissions generator for Pi Coding Agent.
33780
+ *
33781
+ * Pi exposes no allow/ask/deny rule surface, so no canonical permission
33782
+ * category maps onto it. Its one repository-syncable tool gate is `defaultTools` —
33783
+ * the built-in tools enabled at startup (v0.84.2) — authored through the `pi`
33784
+ * permissions override.
33785
+ *
33786
+ * Scope semantics are the **opposite** of the union-merge most targets use:
33787
+ * a project `defaultTools` array *replaces* the global array rather than adding
33788
+ * to it, so the two scopes are written independently and never combined.
33789
+ * Project scope writes `.pi/settings.json`; global scope writes
33790
+ * `~/.pi/agent/settings.json`.
33791
+ *
33792
+ * The file is hand-edited and holds many unrelated keys (`theme`,
33793
+ * `defaultModel`, `packages`, `sessionDir`, ...), so writes go through the
33794
+ * shared-config gateway with `defaultTools` as the only owned key, and the file
33795
+ * is never deleted.
33796
+ *
33797
+ * @see https://pi.dev/docs/latest/settings
33798
+ */
33799
+ var PiPermissions = class PiPermissions extends ToolPermissions {
33800
+ constructor(params) {
33801
+ super({
33802
+ ...params,
33803
+ fileContent: params.fileContent ?? "{}"
33804
+ });
33805
+ }
33806
+ /** `settings.json` holds unrelated user settings, so it must not be deleted. */
33807
+ isDeletable() {
33808
+ return false;
33809
+ }
33810
+ static getSettablePaths({ global = false } = {}) {
33811
+ return {
33812
+ relativeDirPath: global ? PI_AGENT_DIR_PATH : ".pi",
33813
+ relativeFilePath: PI_SETTINGS_FILE_NAME
33814
+ };
33815
+ }
33816
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33817
+ const paths = PiPermissions.getSettablePaths({ global });
33818
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
33819
+ return new PiPermissions({
33820
+ outputRoot,
33821
+ relativeDirPath: paths.relativeDirPath,
33822
+ relativeFilePath: paths.relativeFilePath,
33823
+ fileContent,
33824
+ validate,
33825
+ global
33826
+ });
33827
+ }
33828
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
33829
+ const paths = PiPermissions.getSettablePaths({ global });
33830
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
33831
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
33832
+ const defaultTools = rulesyncPermissions.getJson().pi?.defaultTools;
33833
+ const patch = {};
33834
+ if (defaultTools !== void 0) patch[DEFAULT_TOOLS_KEY] = defaultTools;
33835
+ return new PiPermissions({
33836
+ outputRoot,
33837
+ relativeDirPath: paths.relativeDirPath,
33838
+ relativeFilePath: paths.relativeFilePath,
33839
+ fileContent: applySharedConfigPatch({
33840
+ fileKey: sharedConfigFileKey(paths),
33841
+ feature: "permissions",
33842
+ existingContent,
33843
+ patch,
33844
+ filePath
33845
+ }),
33846
+ validate: true,
33847
+ global
33848
+ });
33849
+ }
33850
+ toRulesyncPermissions() {
33851
+ const defaultTools = this.parseSettings()[DEFAULT_TOOLS_KEY];
33852
+ const config = { permission: {} };
33853
+ if (Array.isArray(defaultTools) && defaultTools.every((tool) => typeof tool === "string")) config.pi = { [DEFAULT_TOOLS_KEY]: defaultTools };
33854
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
33855
+ }
33856
+ parseSettings() {
33857
+ try {
33858
+ return parseSharedConfig({
33859
+ format: "json",
33860
+ fileContent: this.getFileContent()
33861
+ });
33862
+ } catch (error) {
33863
+ throw new Error(`Failed to parse Pi settings in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
33864
+ }
33865
+ }
33866
+ validate() {
33867
+ return {
33868
+ success: true,
33869
+ error: null
33870
+ };
33871
+ }
33872
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
33873
+ return new PiPermissions({
33874
+ outputRoot,
33875
+ relativeDirPath,
33876
+ relativeFilePath,
33877
+ fileContent: "{}",
33878
+ validate: false,
33879
+ global
33880
+ });
33881
+ }
33882
+ };
33883
+ //#endregion
33248
33884
  //#region src/features/permissions/qwencode-permissions.ts
33249
33885
  /**
33250
33886
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -34308,7 +34944,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
34308
34944
  permission[category] ??= {};
34309
34945
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
34310
34946
  }
34311
- if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34947
+ if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34312
34948
  permission.read ??= {};
34313
34949
  permission.read[path] ??= "allow";
34314
34950
  }
@@ -35285,8 +35921,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
35285
35921
  const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
35286
35922
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
35287
35923
  const config = convertWarpToRulesyncPermissions({
35288
- allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35289
- deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35924
+ allow: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35925
+ deny: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35290
35926
  });
35291
35927
  const warpOverride = {};
35292
35928
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -35998,6 +36634,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
35998
36634
  supportsImport: true
35999
36635
  }
36000
36636
  }],
36637
+ ["pi", {
36638
+ class: PiPermissions,
36639
+ meta: {
36640
+ supportsProject: true,
36641
+ supportsGlobal: true,
36642
+ supportsImport: true
36643
+ }
36644
+ }],
36001
36645
  ["qwencode", {
36002
36646
  class: QwencodePermissions,
36003
36647
  meta: {
@@ -36800,11 +37444,12 @@ var AmpSkill = class AmpSkill extends ToolSkill {
36800
37444
  };
36801
37445
  }
36802
37446
  toRulesyncSkill() {
36803
- const frontmatter = this.getFrontmatter();
37447
+ const { name, description, ...ampSection } = this.getFrontmatter();
36804
37448
  const rulesyncFrontmatter = {
36805
- name: frontmatter.name,
36806
- description: frontmatter.description,
36807
- targets: ["*"]
37449
+ name,
37450
+ description,
37451
+ targets: ["*"],
37452
+ ...Object.keys(ampSection).length > 0 && { amp: ampSection }
36808
37453
  };
36809
37454
  return new RulesyncSkill({
36810
37455
  outputRoot: this.outputRoot,
@@ -36821,6 +37466,7 @@ var AmpSkill = class AmpSkill extends ToolSkill {
36821
37466
  const settablePaths = AmpSkill.getSettablePaths({ global });
36822
37467
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
36823
37468
  const ampFrontmatter = {
37469
+ ...rulesyncFrontmatter.amp,
36824
37470
  name: rulesyncFrontmatter.name,
36825
37471
  description: rulesyncFrontmatter.description
36826
37472
  };
@@ -38307,6 +38953,7 @@ const CursorSkillFrontmatterSchema = zod_mini.z.looseObject({
38307
38953
  description: zod_mini.z.string(),
38308
38954
  paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
38309
38955
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
38956
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
38310
38957
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
38311
38958
  });
38312
38959
  /**
@@ -38361,6 +39008,7 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38361
39008
  const cursorSection = {
38362
39009
  ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
38363
39010
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
39011
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
38364
39012
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
38365
39013
  };
38366
39014
  const rulesyncFrontmatter = {
@@ -38388,11 +39036,16 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38388
39036
  rootFrontmatter: rulesyncFrontmatter,
38389
39037
  section: cursorSection
38390
39038
  });
39039
+ const resolvedUserInvocable = resolveUserInvocable({
39040
+ rootFrontmatter: rulesyncFrontmatter,
39041
+ section: cursorSection
39042
+ });
38391
39043
  const cursorFrontmatter = {
38392
39044
  name: rulesyncFrontmatter.name,
38393
39045
  description: rulesyncFrontmatter.description,
38394
39046
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
38395
39047
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
39048
+ ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
38396
39049
  ...cursorSection?.metadata !== void 0 && { metadata: cursorSection.metadata }
38397
39050
  };
38398
39051
  return new CursorSkill({
@@ -39247,6 +39900,47 @@ const JunieSkillFrontmatterSchema = zod_mini.z.looseObject({
39247
39900
  name: zod_mini.z.string(),
39248
39901
  description: zod_mini.z.string()
39249
39902
  });
39903
+ /** An ATX markdown heading line (`#` … `######`). */
39904
+ const HEADING_LINE = /^#{1,6}(\s|$)/;
39905
+ /**
39906
+ * Junie's own fallback for a `SKILL.md` with no `description`: "If
39907
+ * `description` is not provided in the frontmatter, Junie CLI extracts the
39908
+ * first paragraph of the body content as the description." Headings do not
39909
+ * count as that paragraph — "If the body is also empty or contains only
39910
+ * headings, the skill will fail to load."
39911
+ *
39912
+ * This is import-only. The canonical `RulesyncSkillFrontmatter` requires a
39913
+ * description, so without the fallback a skill Junie itself loads fine aborts
39914
+ * the whole import; generation keeps emitting an explicit description, which
39915
+ * the same docs recommend.
39916
+ *
39917
+ * Heading lines are therefore skipped rather than taken: a body opening with
39918
+ * `# Skill Name` would otherwise import that title as the description and —
39919
+ * because the next generate writes it out explicitly — replace Junie's own
39920
+ * correct fallback with the wrong value everywhere, canonical config included.
39921
+ *
39922
+ * A paragraph runs to the first blank line or heading, and is collapsed onto
39923
+ * one line because it becomes a YAML frontmatter value. A fenced code block is
39924
+ * not treated specially: it is ordinary content, so a body whose first
39925
+ * paragraph is a fence yields the fence text. Returns an empty string when the
39926
+ * body holds no such paragraph, which the caller turns into a skipped skill.
39927
+ *
39928
+ * @see https://junie.jetbrains.com/docs/agent-skills.html
39929
+ */
39930
+ function deriveDescriptionFromBody(body) {
39931
+ const paragraph = [];
39932
+ for (const rawLine of body.split(/\r?\n/)) {
39933
+ const line = rawLine.trim();
39934
+ if (paragraph.length === 0) {
39935
+ if (line === "" || HEADING_LINE.test(line)) continue;
39936
+ paragraph.push(line);
39937
+ continue;
39938
+ }
39939
+ if (line === "" || HEADING_LINE.test(line)) break;
39940
+ paragraph.push(line);
39941
+ }
39942
+ return paragraph.join(" ").trim();
39943
+ }
39250
39944
  /**
39251
39945
  * Represents a JetBrains Junie skill directory.
39252
39946
  * Skills are stored under the .junie/skills directory with SKILL.md files.
@@ -39343,7 +40037,16 @@ var JunieSkill = class JunieSkill extends ToolSkill {
39343
40037
  ...params,
39344
40038
  getSettablePaths: JunieSkill.getSettablePaths
39345
40039
  });
39346
- const result = JunieSkillFrontmatterSchema.safeParse(loaded.frontmatter);
40040
+ let frontmatter = loaded.frontmatter;
40041
+ if (isRecord$1(frontmatter) && frontmatter.description === void 0) {
40042
+ const derived = deriveDescriptionFromBody(loaded.body);
40043
+ if (derived === "") throw new Error(`Cannot import ${(0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName, SKILL_FILE_NAME)}: it has no description and its body has no paragraph to derive one from, so Junie cannot load it either. Add a description to the frontmatter.`);
40044
+ frontmatter = {
40045
+ ...frontmatter,
40046
+ description: derived
40047
+ };
40048
+ }
40049
+ const result = JunieSkillFrontmatterSchema.safeParse(frontmatter);
39347
40050
  if (!result.success) {
39348
40051
  const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
39349
40052
  throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
@@ -41808,7 +42511,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
41808
42511
  meta: {
41809
42512
  supportsProject: true,
41810
42513
  supportsSimulated: false,
41811
- supportsGlobal: true
42514
+ supportsGlobal: true,
42515
+ lenientImport: true
41812
42516
  }
41813
42517
  }],
41814
42518
  ["kilo", {
@@ -46344,6 +47048,23 @@ var TaktSubagent = class TaktSubagent extends ToolSubagent {
46344
47048
  };
46345
47049
  //#endregion
46346
47050
  //#region src/features/subagents/vibe-subagent.ts
47051
+ /**
47052
+ * Where Vibe reads a custom system prompt from: `.vibe/prompts/<id>.md`
47053
+ * (project) and `~/.vibe/prompts/<id>.md` (user), with the custom directories
47054
+ * winning over the builtins. The id must be a bare file name with no path
47055
+ * separators.
47056
+ * @see vibe/core/prompts/__init__.py
47057
+ */
47058
+ const VIBE_PROMPTS_DIR_PATH = (0, node_path.join)(".vibe", "prompts");
47059
+ /**
47060
+ * Vibe resolves a `system_prompt_id` by file name, and
47061
+ * `VibeConfigSchema._check_system_prompt` evaluates it during validation — an
47062
+ * unresolvable id raises and the agent is dropped at discovery. The id must
47063
+ * therefore stay a bare slug with no path separators.
47064
+ */
47065
+ function promptSlug(relativeFilePath) {
47066
+ return (0, node_path.basename)(relativeFilePath).replace(/\.(md|toml)$/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
47067
+ }
46347
47068
  const VibeSubagentTomlSchema = zod_mini.z.looseObject({
46348
47069
  agent_type: zod_mini.z.enum(["agent", "subagent"]),
46349
47070
  display_name: zod_mini.z.optional(zod_mini.z.string()),
@@ -46360,14 +47081,22 @@ const VibeSubagentTomlSchema = zod_mini.z.looseObject({
46360
47081
  });
46361
47082
  var VibeSubagent = class VibeSubagent extends ToolSubagent {
46362
47083
  body;
46363
- constructor({ body, ...rest }) {
46364
- if (rest.validate !== false) try {
47084
+ promptFile;
47085
+ resolvedSystemPrompt;
47086
+ constructor({ body, promptFile = false, resolvedSystemPrompt, ...rest }) {
47087
+ if (rest.validate !== false && !promptFile) try {
46365
47088
  VibeSubagentTomlSchema.parse(smol_toml.parse(body));
46366
47089
  } catch (error) {
46367
47090
  throw new Error(`Invalid TOML in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(error)}`, { cause: error });
46368
47091
  }
46369
47092
  super({ ...rest });
46370
47093
  this.body = body;
47094
+ this.promptFile = promptFile;
47095
+ this.resolvedSystemPrompt = resolvedSystemPrompt;
47096
+ }
47097
+ /** True for the companion `.vibe/prompts/<id>.md` file. */
47098
+ isPromptFile() {
47099
+ return this.promptFile;
46371
47100
  }
46372
47101
  static getSettablePaths(_options = {}) {
46373
47102
  return { relativeDirPath: (0, node_path.join)(".vibe", "agents") };
@@ -46382,7 +47111,9 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46382
47111
  } catch (error) {
46383
47112
  throw new Error(`Failed to parse TOML in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
46384
47113
  }
46385
- const { system_prompt, description, display_name, ...vibeSection } = parsed;
47114
+ const { system_prompt, system_prompt_id, description, display_name, ...vibeSection } = parsed;
47115
+ const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
47116
+ const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
46386
47117
  const fileStem = (0, node_path.basename)(this.getRelativeFilePath(), ".toml");
46387
47118
  const rulesyncFrontmatter = {
46388
47119
  targets: ["vibe"],
@@ -46391,13 +47122,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46391
47122
  vibe: {
46392
47123
  ...display_name !== void 0 && { display_name },
46393
47124
  ...description !== void 0 && { description },
47125
+ ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
46394
47126
  ...vibeSection
46395
47127
  }
46396
47128
  };
46397
47129
  return new RulesyncSubagent({
46398
47130
  outputRoot: this.outputRoot,
46399
47131
  frontmatter: rulesyncFrontmatter,
46400
- body: system_prompt ?? "",
47132
+ body: resolvedBody ?? "",
46401
47133
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
46402
47134
  relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
46403
47135
  validate: true
@@ -46412,12 +47144,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46412
47144
  "description",
46413
47145
  "system_prompt"
46414
47146
  ]);
47147
+ const promptBody = rulesyncSubagent.getBody();
47148
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
46415
47149
  const tomlObj = {
46416
47150
  agent_type: rawSection.agent_type === "agent" ? "agent" : "subagent",
46417
47151
  display_name: typeof rawSection.display_name === "string" ? rawSection.display_name : frontmatter.name,
46418
47152
  ...frontmatter.description ? { description: frontmatter.description } : {},
46419
- ...rulesyncSubagent.getBody() ? { system_prompt: rulesyncSubagent.getBody() } : {},
46420
- ...vibeSection
47153
+ ...vibeSection,
47154
+ ...promptBody ? { system_prompt_id: slug } : {}
46421
47155
  };
46422
47156
  const body = smol_toml.stringify(tomlObj);
46423
47157
  const paths = this.getSettablePaths({ global });
@@ -46432,6 +47166,36 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46432
47166
  global
46433
47167
  });
46434
47168
  }
47169
+ /**
47170
+ * Emits the agent TOML plus its companion prompt file. The prompt file MUST
47171
+ * be written together with the id: `VibeConfigSchema._check_system_prompt`
47172
+ * evaluates the property during validation, so an unresolvable
47173
+ * `system_prompt_id` raises and `AgentRegistry._try_load` drops the agent
47174
+ * with a warning.
47175
+ */
47176
+ static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false }) {
47177
+ return rulesyncSubagents.flatMap((rulesyncSubagent) => {
47178
+ const agentFile = VibeSubagent.fromRulesyncSubagent({
47179
+ outputRoot,
47180
+ relativeDirPath: VibeSubagent.getSettablePaths({ global }).relativeDirPath,
47181
+ rulesyncSubagent,
47182
+ global
47183
+ });
47184
+ const promptBody = rulesyncSubagent.getBody();
47185
+ if (!promptBody) return [agentFile];
47186
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
47187
+ return [agentFile, new VibeSubagent({
47188
+ outputRoot,
47189
+ body: promptBody,
47190
+ relativeDirPath: VIBE_PROMPTS_DIR_PATH,
47191
+ relativeFilePath: `${slug}.md`,
47192
+ fileContent: promptBody,
47193
+ promptFile: true,
47194
+ validate: false,
47195
+ global
47196
+ })];
47197
+ });
47198
+ }
46435
47199
  validate() {
46436
47200
  try {
46437
47201
  VibeSubagentTomlSchema.parse(smol_toml.parse(this.body));
@@ -46456,6 +47220,10 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46456
47220
  const paths = this.getSettablePaths({ global });
46457
47221
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
46458
47222
  const fileContent = await readFileContent(filePath);
47223
+ const resolvedSystemPrompt = await VibeSubagent.readSystemPrompt({
47224
+ outputRoot,
47225
+ fileContent
47226
+ });
46459
47227
  const subagent = new VibeSubagent({
46460
47228
  outputRoot,
46461
47229
  relativeDirPath: paths.relativeDirPath,
@@ -46463,7 +47231,8 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46463
47231
  body: fileContent.trim(),
46464
47232
  fileContent,
46465
47233
  validate,
46466
- global
47234
+ global,
47235
+ ...resolvedSystemPrompt !== void 0 && { resolvedSystemPrompt }
46467
47236
  });
46468
47237
  if (validate) {
46469
47238
  const result = subagent.validate();
@@ -46471,6 +47240,21 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46471
47240
  }
46472
47241
  return subagent;
46473
47242
  }
47243
+ /**
47244
+ * Read `.vibe/prompts/<id>.md` for an agent that names a `system_prompt_id`.
47245
+ * Returns `undefined` when there is no id or the file is missing, in which
47246
+ * case import falls back to the legacy `system_prompt` key.
47247
+ */
47248
+ static async readSystemPrompt({ outputRoot, fileContent }) {
47249
+ let promptId;
47250
+ try {
47251
+ promptId = VibeSubagentTomlSchema.parse(smol_toml.parse(fileContent)).system_prompt_id;
47252
+ } catch {
47253
+ return;
47254
+ }
47255
+ if (typeof promptId !== "string" || promptId === "" || /[\\/]/.test(promptId)) return;
47256
+ return await readFileContentOrNull((0, node_path.join)(outputRoot, VIBE_PROMPTS_DIR_PATH, `${promptId}.md`)) ?? void 0;
47257
+ }
46474
47258
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46475
47259
  return new VibeSubagent({
46476
47260
  outputRoot,
@@ -46513,7 +47297,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46513
47297
  const mode = super.toRooMode(rulesyncSubagent);
46514
47298
  const frontmatter = rulesyncSubagent.getFrontmatter();
46515
47299
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
46516
- if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
47300
+ if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
46517
47301
  return mode;
46518
47302
  }
46519
47303
  toRulesyncSubagents() {
@@ -46525,7 +47309,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46525
47309
  targets: ["zoocode"],
46526
47310
  roo: restRooSection
46527
47311
  };
46528
- if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
47312
+ if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
46529
47313
  return new RulesyncSubagent({
46530
47314
  outputRoot: ".",
46531
47315
  frontmatter: rebuilt,
@@ -49428,8 +50212,13 @@ const STRATEGIES = [
49428
50212
  * file per rule under `.devin/rules/*.md` (Devin Desktop Cascade) with YAML
49429
50213
  * frontmatter carrying a `trigger` (always_on | glob | manual | model_decision)
49430
50214
  * plus companion `globs`/`description` fields.
49431
- * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
49432
- * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
50215
+ * - Global scope: mirrors the project layout. The root rule is a single
50216
+ * plain-markdown, always-on file (no frontmatter) at
50217
+ * `~/.config/devin/AGENTS.md`; non-root rules are one file per rule under
50218
+ * `~/.devin/rules/*.md` with the same `trigger`/`globs` frontmatter as project
50219
+ * scope. Note the directory split — the per-rule global directory is the home
50220
+ * `~/.devin/`, not the `~/.config/devin/` tree every other Devin global
50221
+ * surface uses. That is what the rules page documents.
49433
50222
  *
49434
50223
  * Trigger inference (when no explicit devin trigger is persisted):
49435
50224
  * - Specific globs (non wildcard) → glob
@@ -49460,19 +50249,32 @@ var DevinRule = class DevinRule extends ToolRule {
49460
50249
  relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
49461
50250
  };
49462
50251
  }
50252
+ /**
50253
+ * The always-on root rule file for a scope: the project-root `AGENTS.md`
50254
+ * Devin CLI / Devin Local reads, or `~/.config/devin/AGENTS.md` in global
50255
+ * scope. Single-sourced so `getSettablePaths`, `fromFile` and
50256
+ * `fromRulesyncRule` cannot disagree about it.
50257
+ */
50258
+ static getRootPath(global, excludeToolDir) {
50259
+ return global ? DevinRule.getGlobalRootPath(excludeToolDir) : {
50260
+ relativeDirPath: ".",
50261
+ relativeFilePath: "AGENTS.md"
50262
+ };
50263
+ }
49463
50264
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
49464
- if (global) return { root: DevinRule.getGlobalRootPath(excludeToolDir) };
50265
+ if (global) return {
50266
+ root: DevinRule.getRootPath(true, excludeToolDir),
50267
+ nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
50268
+ };
49465
50269
  return {
49466
- root: {
49467
- relativeDirPath: ".",
49468
- relativeFilePath: "AGENTS.md"
49469
- },
50270
+ root: DevinRule.getRootPath(false, excludeToolDir),
49470
50271
  nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
49471
50272
  };
49472
50273
  }
49473
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
49474
- if (global) {
49475
- const rootPath = DevinRule.getGlobalRootPath();
50274
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
50275
+ const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
50276
+ if (relativeDirPath !== void 0 ? relativeDirPath !== nonRootDirPath : relativeFilePath === DevinRule.getRootPath(global).relativeFilePath) {
50277
+ const rootPath = DevinRule.getRootPath(global);
49476
50278
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
49477
50279
  return new DevinRule({
49478
50280
  outputRoot,
@@ -49484,19 +50286,6 @@ var DevinRule = class DevinRule extends ToolRule {
49484
50286
  root: true
49485
50287
  });
49486
50288
  }
49487
- if (relativeFilePath === "AGENTS.md") {
49488
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeFilePath));
49489
- return new DevinRule({
49490
- outputRoot,
49491
- relativeDirPath: ".",
49492
- relativeFilePath: "AGENTS.md",
49493
- frontmatter: {},
49494
- body: fileContent,
49495
- validate,
49496
- root: true
49497
- });
49498
- }
49499
- const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
49500
50289
  const filePath = (0, node_path.join)(outputRoot, nonRootDirPath, relativeFilePath);
49501
50290
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
49502
50291
  let parsedFrontmatter;
@@ -49516,8 +50305,9 @@ var DevinRule = class DevinRule extends ToolRule {
49516
50305
  });
49517
50306
  }
49518
50307
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
49519
- if (global) {
49520
- const rootPath = DevinRule.getGlobalRootPath();
50308
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
50309
+ if (rulesyncFrontmatter.root) {
50310
+ const rootPath = DevinRule.getRootPath(global);
49521
50311
  return new DevinRule({
49522
50312
  outputRoot,
49523
50313
  relativeDirPath: rootPath.relativeDirPath,
@@ -49528,16 +50318,6 @@ var DevinRule = class DevinRule extends ToolRule {
49528
50318
  root: true
49529
50319
  });
49530
50320
  }
49531
- const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
49532
- if (rulesyncFrontmatter.root) return new DevinRule({
49533
- outputRoot,
49534
- relativeDirPath: ".",
49535
- relativeFilePath: "AGENTS.md",
49536
- frontmatter: {},
49537
- body: rulesyncRule.getBody(),
49538
- validate,
49539
- root: true
49540
- });
49541
50321
  const storedDevin = rulesyncFrontmatter.devin;
49542
50322
  const normalized = normalizeStoredDevin(storedDevin);
49543
50323
  const storedTrigger = storedDevin?.trigger;
@@ -49597,7 +50377,7 @@ var DevinRule = class DevinRule extends ToolRule {
49597
50377
  error: null
49598
50378
  };
49599
50379
  }
49600
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50380
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
49601
50381
  return new DevinRule({
49602
50382
  outputRoot,
49603
50383
  relativeDirPath,
@@ -49605,7 +50385,7 @@ var DevinRule = class DevinRule extends ToolRule {
49605
50385
  frontmatter: {},
49606
50386
  body: "",
49607
50387
  validate: false,
49608
- root: global
50388
+ root: relativeDirPath !== buildToolPath(DEVIN_DIR, "rules")
49609
50389
  });
49610
50390
  }
49611
50391
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -50238,6 +51018,11 @@ var KimiCodeRule = class KimiCodeRule extends ToolRule {
50238
51018
  };
50239
51019
  //#endregion
50240
51020
  //#region src/features/rules/kiro-rule.ts
51021
+ /**
51022
+ * Kiro loads `AGENTS.md` as steering context from any directory in the
51023
+ * workspace tree (CLI 2.18.0 / IDE 1.0.309), not just the workspace root.
51024
+ */
51025
+ const KIRO_NESTED_STEERING_FILE_NAME = "AGENTS.md";
50241
51026
  const WILDCARD_GLOBS = /* @__PURE__ */ new Set([
50242
51027
  "**/*",
50243
51028
  "**",
@@ -50311,8 +51096,17 @@ var KiroRule = class KiroRule extends ToolRule {
50311
51096
  };
50312
51097
  return { nonRoot: { relativeDirPath: steeringDirPath } };
50313
51098
  }
50314
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
51099
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
50315
51100
  const paths = this.getSettablePaths({ global });
51101
+ const steeringDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
51102
+ if (overrideDirPath !== void 0 && overrideDirPath !== steeringDirPath && overrideDirPath !== "." && relativeFilePath === KIRO_NESTED_STEERING_FILE_NAME) return new KiroRule({
51103
+ outputRoot,
51104
+ relativeDirPath: overrideDirPath,
51105
+ relativeFilePath,
51106
+ fileContent: await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, relativeFilePath)),
51107
+ validate,
51108
+ root: false
51109
+ });
50316
51110
  const isRoot = "root" in paths && relativeFilePath === paths.root.relativeFilePath;
50317
51111
  const relativeDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
50318
51112
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
@@ -50336,6 +51130,12 @@ var KiroRule = class KiroRule extends ToolRule {
50336
51130
  });
50337
51131
  if (params.root) return new KiroRule(params);
50338
51132
  const frontmatter = rulesyncRule.getFrontmatter();
51133
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
51134
+ if (!global && subprojectPath) return new KiroRule({
51135
+ ...params,
51136
+ relativeDirPath: (0, node_path.join)(subprojectPath),
51137
+ relativeFilePath: KIRO_NESTED_STEERING_FILE_NAME
51138
+ });
50339
51139
  const inclusion = deriveKiroInclusion({
50340
51140
  kiro: frontmatter.kiro,
50341
51141
  globs: frontmatter.globs
@@ -50346,7 +51146,34 @@ var KiroRule = class KiroRule extends ToolRule {
50346
51146
  fileContent: stringifyFrontmatter(rulesyncRule.getBody(), inclusion)
50347
51147
  });
50348
51148
  }
51149
+ /**
51150
+ * The subproject directory a nested `AGENTS.md` scopes, or `undefined` for the
51151
+ * root rule and for the `.kiro/steering/` files.
51152
+ */
51153
+ getSubprojectPath() {
51154
+ if (this.isRoot() || this.getRelativeFilePath() !== KIRO_NESTED_STEERING_FILE_NAME) return;
51155
+ const relativeDirPath = toPosixPath(this.getRelativeDirPath());
51156
+ if (relativeDirPath === "." || relativeDirPath === "" || relativeDirPath.startsWith(".")) return;
51157
+ return relativeDirPath;
51158
+ }
50349
51159
  toRulesyncRule() {
51160
+ const subprojectPath = this.getSubprojectPath();
51161
+ if (subprojectPath !== void 0) {
51162
+ const slug = subprojectPath.replaceAll("/", "-");
51163
+ return new RulesyncRule({
51164
+ outputRoot: this.getOutputRoot(),
51165
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
51166
+ relativeFilePath: `${slug}-kiro.md`,
51167
+ frontmatter: {
51168
+ targets: ["kiro"],
51169
+ root: false,
51170
+ globs: [`${subprojectPath}/**/*`],
51171
+ agentsmd: { subprojectPath }
51172
+ },
51173
+ body: this.getFileContent(),
51174
+ validate: true
51175
+ });
51176
+ }
50350
51177
  const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getRelativeFilePath());
50351
51178
  const inclusion = typeof frontmatter.inclusion === "string" ? frontmatter.inclusion : void 0;
50352
51179
  if (!inclusion) return this.toRulesyncRuleDefault();
@@ -50379,6 +51206,26 @@ var KiroRule = class KiroRule extends ToolRule {
50379
51206
  error: null
50380
51207
  };
50381
51208
  }
51209
+ /**
51210
+ * Nested `AGENTS.md` files anywhere in the workspace tree, which Kiro loads as
51211
+ * steering context alongside `.kiro/steering/`. Import-only, like every other
51212
+ * nested scan: these are hand-authored files outside a rulesync-owned
51213
+ * directory, so enumerating them for `--delete` would sweep away work rulesync
51214
+ * never wrote.
51215
+ * @see https://kiro.dev/docs/steering/
51216
+ */
51217
+ static getNestedFilePatterns({ outputRoot }) {
51218
+ const root = toPosixPath(outputRoot);
51219
+ return {
51220
+ include: [`${root}/**/${KIRO_NESTED_STEERING_FILE_NAME}`],
51221
+ ignore: [
51222
+ `${root}/${KIRO_NESTED_STEERING_FILE_NAME}`,
51223
+ `${root}/**/.*/**`,
51224
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
51225
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
51226
+ ]
51227
+ };
51228
+ }
50382
51229
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50383
51230
  return new KiroRule({
50384
51231
  outputRoot,
@@ -51847,6 +52694,18 @@ const resolveIncludeLocalRoot = (options) => {
51847
52694
  return parsed.data.includeLocalRoot ?? true;
51848
52695
  };
51849
52696
  /**
52697
+ * Whether `candidate` is `base` itself or a class that extends it.
52698
+ *
52699
+ * Class-identity dispatch (`factory.class === Base`) is subclass-blind: a target
52700
+ * whose adapter merely narrows another one — `ZoocodeRule extends RooRule` — fails
52701
+ * a strict `===` and silently falls through. That is how `zoocode` ended up
52702
+ * declaring `localRootMode: "separate-local-file"` while never emitting
52703
+ * `AGENTS.local.md`, in both directions and without a warning.
52704
+ */
52705
+ function isClassOrSubclassOf({ candidate, base }) {
52706
+ return candidate === base || Object.prototype.isPrototypeOf.call(base, candidate);
52707
+ }
52708
+ /**
51850
52709
  * Factory Map mapping tool targets to their rule factories.
51851
52710
  * Using Map to preserve insertion order for consistent iteration.
51852
52711
  */
@@ -52200,7 +53059,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
52200
53059
  meta: {
52201
53060
  extension: "md",
52202
53061
  supportsGlobal: true,
52203
- ruleDiscoveryMode: "auto"
53062
+ ruleDiscoveryMode: "auto",
53063
+ localRootMode: "separate-local-file",
53064
+ localRootFileName: "AGENTS.local.md"
52204
53065
  }
52205
53066
  }],
52206
53067
  ["zed", {
@@ -52487,7 +53348,10 @@ var RulesProcessor = class extends FeatureProcessor {
52487
53348
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
52488
53349
  }
52489
53350
  buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
52490
- if (factory.class === ClaudecodeRule) {
53351
+ if (isClassOrSubclassOf({
53352
+ candidate: factory.class,
53353
+ base: ClaudecodeRule
53354
+ })) {
52491
53355
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
52492
53356
  return new ClaudecodeRule({
52493
53357
  outputRoot: this.outputRoot,
@@ -52500,7 +53364,10 @@ var RulesProcessor = class extends FeatureProcessor {
52500
53364
  localRoot
52501
53365
  });
52502
53366
  }
52503
- if (factory.class === ClaudecodeLegacyRule) {
53367
+ if (isClassOrSubclassOf({
53368
+ candidate: factory.class,
53369
+ base: ClaudecodeLegacyRule
53370
+ })) {
52504
53371
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
52505
53372
  return new ClaudecodeLegacyRule({
52506
53373
  outputRoot: this.outputRoot,
@@ -52512,7 +53379,10 @@ var RulesProcessor = class extends FeatureProcessor {
52512
53379
  localRoot
52513
53380
  });
52514
53381
  }
52515
- if (factory.class === RovodevRule) return new RovodevRule({
53382
+ if (isClassOrSubclassOf({
53383
+ candidate: factory.class,
53384
+ base: RovodevRule
53385
+ })) return new RovodevRule({
52516
53386
  outputRoot: this.outputRoot,
52517
53387
  relativeDirPath: relativeDirPath ?? ".",
52518
53388
  relativeFilePath: fileName,
@@ -52521,7 +53391,10 @@ var RulesProcessor = class extends FeatureProcessor {
52521
53391
  root: true,
52522
53392
  localRoot
52523
53393
  });
52524
- if (factory.class === RooRule) return new RooRule({
53394
+ if (isClassOrSubclassOf({
53395
+ candidate: factory.class,
53396
+ base: RooRule
53397
+ })) return new RooRule({
52525
53398
  outputRoot: this.outputRoot,
52526
53399
  relativeDirPath: relativeDirPath ?? ".",
52527
53400
  relativeFilePath: fileName,
@@ -52530,7 +53403,23 @@ var RulesProcessor = class extends FeatureProcessor {
52530
53403
  root: true,
52531
53404
  localRoot
52532
53405
  });
52533
- if (factory.class === QwencodeRule) return new QwencodeRule({
53406
+ if (isClassOrSubclassOf({
53407
+ candidate: factory.class,
53408
+ base: DevinRule
53409
+ })) return new DevinRule({
53410
+ outputRoot: this.outputRoot,
53411
+ relativeDirPath: relativeDirPath ?? ".",
53412
+ relativeFilePath: fileName,
53413
+ frontmatter: {},
53414
+ body,
53415
+ validate: true,
53416
+ root: true,
53417
+ localRoot
53418
+ });
53419
+ if (isClassOrSubclassOf({
53420
+ candidate: factory.class,
53421
+ base: QwencodeRule
53422
+ })) return new QwencodeRule({
52534
53423
  outputRoot: this.outputRoot,
52535
53424
  relativeDirPath: relativeDirPath ?? ".qwen",
52536
53425
  relativeFilePath: fileName,