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.
@@ -412,6 +412,7 @@ const permissionsProcessorToolTargetTuple = [
412
412
  "kiro-cli",
413
413
  "kiro-ide",
414
414
  "opencode",
415
+ "pi",
415
416
  "qwencode",
416
417
  "reasonix",
417
418
  "rovodev",
@@ -866,6 +867,7 @@ const ErrorCodes = {
866
867
  INIT_FAILED: "INIT_FAILED",
867
868
  MCP_FAILED: "MCP_FAILED",
868
869
  DOCTOR_FAILED: "DOCTOR_FAILED",
870
+ RELEASE_NOTES_FAILED: "RELEASE_NOTES_FAILED",
869
871
  UNKNOWN_ERROR: "UNKNOWN_ERROR"
870
872
  };
871
873
  /**
@@ -1902,7 +1904,7 @@ function isPlainObject$1(value) {
1902
1904
  /**
1903
1905
  * Type guard to check if a value is an array of strings.
1904
1906
  */
1905
- function isStringArray$1(value) {
1907
+ function isStringArray$2(value) {
1906
1908
  return Array.isArray(value) && value.every((item) => typeof item === "string");
1907
1909
  }
1908
1910
  //#endregion
@@ -2270,6 +2272,7 @@ const HOOK_EVENTS = [
2270
2272
  "todoCreated",
2271
2273
  "todoCompleted",
2272
2274
  "stopFailure",
2275
+ "stopCancelled",
2273
2276
  "instructionsLoaded",
2274
2277
  "userPromptExpansion",
2275
2278
  "postToolBatch",
@@ -2452,34 +2455,52 @@ const AMP_HOOK_EVENTS = [
2452
2455
  ];
2453
2456
  /**
2454
2457
  * Hook events supported by Cline's file-based hooks. Cline resolves one
2455
- * executable per lifecycle event from its hooks directory, and the event names
2456
- * it accepts are fixed by `VALID_HOOK_TYPES` in
2457
- * `apps/vscode/src/core/hooks/utils.ts`: `TaskStart`, `TaskResume`,
2458
- * `TaskCancel`, `TaskComplete`, `PreToolUse`, `PostToolUse`,
2459
- * `UserPromptSubmit`, `Notification` and `PreCompact`.
2458
+ * executable per lifecycle event from its hooks directory, and the accepted
2459
+ * event names come from two runtimes that read the same directory:
2460
+ *
2461
+ * - The VS Code extension fixes them in `VALID_HOOK_TYPES`
2462
+ * (`apps/vscode/src/core/hooks/utils.ts`): `TaskStart`, `TaskResume`,
2463
+ * `TaskCancel`, `TaskComplete`, `PreToolUse`, `PostToolUse`,
2464
+ * `UserPromptSubmit`, `Notification` and `PreCompact`.
2465
+ * - The SDK/CLI fixes them in `HookConfigFileName`
2466
+ * (`sdk/packages/core/src/hooks/hook-file-config.ts`), which drops
2467
+ * `Notification` but adds `TaskError` (→ `agent_error`) and
2468
+ * `SessionShutdown` (→ `session_shutdown`).
2469
+ *
2470
+ * This set is the union, because `.clinerules/hooks` is in both runtimes'
2471
+ * search paths and a script named for an event the running one does not know
2472
+ * is simply never spawned. That holds for unknown *names* only: for an event a
2473
+ * runtime does know, the SDK/CLI spawns both the extensionless script and its
2474
+ * `.ps1` twin, so each generated script opens with a guard that stands down on
2475
+ * the platform the other one owns — see `generateClineHookScript` and
2476
+ * `generateClineHookPowerShellScript`.
2460
2477
  *
2461
2478
  * `TaskResume` and `TaskCancel` have no canonical counterpart and stay
2462
2479
  * unmapped rather than being approximated by `sessionEnd` / `stop`, whose
2463
2480
  * semantics differ.
2464
2481
  *
2465
2482
  * @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
2483
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/hooks/hook-file-config.ts
2466
2484
  */
2467
2485
  const CLINE_HOOK_EVENTS = [
2468
2486
  "sessionStart",
2487
+ "sessionEnd",
2469
2488
  "preToolUse",
2470
2489
  "postToolUse",
2471
2490
  "beforeSubmitPrompt",
2472
2491
  "preCompact",
2473
2492
  "notification",
2474
- "taskCompleted"
2493
+ "taskCompleted",
2494
+ "afterError"
2475
2495
  ];
2476
2496
  /**
2477
2497
  * Hook events supported by GitHub Copilot (cloud coding agent).
2478
2498
  *
2479
2499
  * The events rulesync writes to `.github/hooks/*.json`:
2480
2500
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted` ← `beforeSubmitPrompt`,
2481
- * `preToolUse`, `postToolUse`, `agentStop` ← `stop`, `subagentStart`,
2482
- * `subagentStop`, `errorOccurred` ← `afterError`, and `preCompact`.
2501
+ * `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` ← `stop`,
2502
+ * `subagentStart`, `subagentStop`, `errorOccurred` ← `afterError`,
2503
+ * `preCompact`, and `userPromptTransformed` ← `userPromptExpansion`.
2483
2504
  *
2484
2505
  * `preCompact` and `subagentStart` are authorable because the unified hooks
2485
2506
  * reference's per-event "Cloud agent" column says both fire there. That column
@@ -2488,9 +2509,11 @@ const CLINE_HOOK_EVENTS = [
2488
2509
  * undo that. `notification` and `permissionRequest` stay out because the same
2489
2510
  * column is explicit that they do not fire on the cloud agent.
2490
2511
  *
2491
- * Two further CLI events — `postToolUseFailure` and `userPromptTransformed`
2492
- * ({@link COPILOTCLI_HOOK_EVENTS}) are documented as firing on the cloud
2493
- * agent as well but are not modelled here yet.
2512
+ * `postToolUseFailure` and `userPromptTransformed` are shared with
2513
+ * {@link COPILOTCLI_HOOK_EVENTS}; the same column records both as firing on the
2514
+ * cloud agent, so they are authorable here too. The event surfaces overlap but
2515
+ * the config surfaces do not: `copilot` emits `command` hooks only, while the
2516
+ * CLI adapter also handles `http` and `prompt`.
2494
2517
  *
2495
2518
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
2496
2519
  */
@@ -2500,11 +2523,13 @@ const COPILOT_HOOK_EVENTS = [
2500
2523
  "beforeSubmitPrompt",
2501
2524
  "preToolUse",
2502
2525
  "postToolUse",
2526
+ "postToolUseFailure",
2503
2527
  "stop",
2504
2528
  "subagentStart",
2505
2529
  "subagentStop",
2506
2530
  "afterError",
2507
- "preCompact"
2531
+ "preCompact",
2532
+ "userPromptExpansion"
2508
2533
  ];
2509
2534
  /**
2510
2535
  * Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
@@ -2823,6 +2848,7 @@ const GROKCLI_HOOK_EVENTS = [
2823
2848
  "permissionDenied",
2824
2849
  "stop",
2825
2850
  "stopFailure",
2851
+ "stopCancelled",
2826
2852
  "notification",
2827
2853
  "subagentStart",
2828
2854
  "subagentStop",
@@ -2898,16 +2924,10 @@ const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CAN
2898
2924
  /**
2899
2925
  * Hook events supported by Hermes Agent's native Shell Hooks system.
2900
2926
  *
2901
- * Hermes validates hook events against a fixed `VALID_HOOKS` set:
2902
- * `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
2903
- * `pre_verify`, `pre_api_request`, `post_api_request`, `api_request_error`,
2904
- * `on_session_start`, `on_session_end`, `on_session_finalize`,
2905
- * `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
2906
- * `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
2907
- * `transform_terminal_output`, `transform_llm_output`, and the three
2908
- * `kanban_task_*` events. Only the events with a clean 1:1 canonical equivalent
2909
- * are mapped here. All other native events round-trip through
2910
- * `hermesagent.hooks`.
2927
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 37 entries as
2928
+ * of v0.20.2 (`v2026.8.16`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
2929
+ * full list. Only the events with a clean 1:1 canonical equivalent are mapped
2930
+ * here. All other native events round-trip through `hermesagent.hooks`.
2911
2931
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
2912
2932
  */
2913
2933
  const HERMESAGENT_HOOK_EVENTS = [
@@ -2920,6 +2940,21 @@ const HERMESAGENT_HOOK_EVENTS = [
2920
2940
  "subagentStart",
2921
2941
  "subagentStop"
2922
2942
  ];
2943
+ /**
2944
+ * Hermes Agent's native `VALID_HOOKS` set, in upstream source order.
2945
+ *
2946
+ * The authority is `hermes_cli/plugins.py`'s `VALID_HOOKS`, not the docs: at
2947
+ * v0.20.2 `website/docs/user-guide/features/plugins.md` says "26 lifecycle
2948
+ * events" and then lists 30, omitting the five `on_kanban_*` worker/task/dispatch
2949
+ * observers, `gateway_platform_event` and `transform_api_error_classification`.
2950
+ * Shell hooks are gated on the same set.
2951
+ *
2952
+ * Verified against the tag `v2026.8.16` (v0.20.2): `VALID_HOOKS` holds 37
2953
+ * entries, grown from 23 at `v2026.8.3` (v0.20.0). This list holds 36 of them —
2954
+ * see {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
2955
+ *
2956
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py
2957
+ */
2923
2958
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2924
2959
  "pre_tool_call",
2925
2960
  "post_tool_call",
@@ -2928,6 +2963,10 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2928
2963
  "transform_llm_output",
2929
2964
  "pre_llm_call",
2930
2965
  "post_llm_call",
2966
+ "on_stream_start",
2967
+ "on_stream_delta",
2968
+ "on_stream_end",
2969
+ "on_interim_message",
2931
2970
  "pre_verify",
2932
2971
  "pre_api_request",
2933
2972
  "post_api_request",
@@ -2936,16 +2975,39 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
2936
2975
  "on_session_end",
2937
2976
  "on_session_finalize",
2938
2977
  "on_session_reset",
2978
+ "on_skill_lifecycle",
2939
2979
  "subagent_start",
2940
2980
  "subagent_stop",
2941
2981
  "pre_gateway_dispatch",
2942
2982
  "pre_approval_request",
2943
2983
  "post_approval_response",
2984
+ "pre_transcription",
2944
2985
  "kanban_task_claimed",
2945
2986
  "kanban_task_completed",
2946
- "kanban_task_blocked"
2987
+ "kanban_task_blocked",
2988
+ "on_kanban_worker_spawned",
2989
+ "on_kanban_worker_exited",
2990
+ "on_kanban_worker_stale_claim",
2991
+ "on_kanban_task_updated",
2992
+ "on_kanban_dispatch_tick",
2993
+ "gateway_platform_event",
2994
+ "pre_command"
2947
2995
  ];
2948
2996
  /**
2997
+ * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
2998
+ * as the shell-hook allow-list, but `SHELL_UNSUPPORTED_HOOKS` is subtracted from
2999
+ * it first: shell hooks cannot return these events' directives, so
3000
+ * `_parse_hooks_block` refuses the registration with a warning rather than
3001
+ * accepting it and dropping the return value.
3002
+ *
3003
+ * rulesync only ever writes shell hooks, so these are excluded from
3004
+ * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
3005
+ *
3006
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
3007
+ * @see https://github.com/NousResearch/hermes-agent/blob/v2026.8.16/agent/shell_hooks.py — `_parse_hooks_block`
3008
+ */
3009
+ const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
3010
+ /**
2949
3011
  * Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
2950
3012
  * snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
2951
3013
  */
@@ -3220,15 +3282,21 @@ const CANONICAL_TO_AMP_EVENT_NAMES = {
3220
3282
  beforeSubmitPrompt: "agent.start",
3221
3283
  stop: "agent.end"
3222
3284
  };
3223
- /** Map canonical hook events to Cline's `VALID_HOOK_TYPES` file names. */
3285
+ /**
3286
+ * Map canonical hook events to Cline's hook script file names — the union of
3287
+ * the VS Code extension's `VALID_HOOK_TYPES` and the SDK/CLI's
3288
+ * `HookConfigFileName`, see {@link CLINE_HOOK_EVENTS}.
3289
+ */
3224
3290
  const CANONICAL_TO_CLINE_EVENT_NAMES = {
3225
3291
  sessionStart: "TaskStart",
3292
+ sessionEnd: "SessionShutdown",
3226
3293
  preToolUse: "PreToolUse",
3227
3294
  postToolUse: "PostToolUse",
3228
3295
  beforeSubmitPrompt: "UserPromptSubmit",
3229
3296
  preCompact: "PreCompact",
3230
3297
  notification: "Notification",
3231
- taskCompleted: "TaskComplete"
3298
+ taskCompleted: "TaskComplete",
3299
+ afterError: "TaskError"
3232
3300
  };
3233
3301
  /**
3234
3302
  * Map canonical camelCase event names to Copilot camelCase.
@@ -3239,11 +3307,13 @@ const CANONICAL_TO_COPILOT_EVENT_NAMES = {
3239
3307
  beforeSubmitPrompt: "userPromptSubmitted",
3240
3308
  preToolUse: "preToolUse",
3241
3309
  postToolUse: "postToolUse",
3310
+ postToolUseFailure: "postToolUseFailure",
3242
3311
  stop: "agentStop",
3243
3312
  subagentStart: "subagentStart",
3244
3313
  subagentStop: "subagentStop",
3245
3314
  afterError: "errorOccurred",
3246
- preCompact: "preCompact"
3315
+ preCompact: "preCompact",
3316
+ userPromptExpansion: "userPromptTransformed"
3247
3317
  };
3248
3318
  /**
3249
3319
  * Map Copilot camelCase event names to canonical camelCase.
@@ -3554,6 +3624,7 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
3554
3624
  permissionDenied: "PermissionDenied",
3555
3625
  stop: "Stop",
3556
3626
  stopFailure: "StopFailure",
3627
+ stopCancelled: "StopCancelled",
3557
3628
  notification: "Notification",
3558
3629
  subagentStart: "SubagentStart",
3559
3630
  subagentStop: "SubagentStop",
@@ -3828,6 +3899,25 @@ const McpServerSchema = z.looseObject({
3828
3899
  kiroAutoApprove: z.optional(z.array(z.string())),
3829
3900
  kiroAutoBlock: z.optional(z.array(z.string())),
3830
3901
  headers: z.optional(z.record(z.string(), z.string())),
3902
+ /**
3903
+ * The canonical per-server tool allowlist.
3904
+ *
3905
+ * **Collision rule for adapters.** Several tools spell an allowlist natively
3906
+ * under a name that also exists here, so a single server entry can carry both
3907
+ * a canonical field and its native counterpart. What the *native* key means
3908
+ * decides the resolution — not which one was written first:
3909
+ *
3910
+ * - Native key is a redundant spelling of the same concept, and both lists are
3911
+ * additive → **merge** them, so a config that already spells the field
3912
+ * natively keeps working (`kiroAutoApprove`/`kiroAutoBlock` in
3913
+ * `kiro-mcp.ts`).
3914
+ * - Native key is the same concept but rulesync owns the generated value, so
3915
+ * keeping both would be ambiguous → the **canonical value wins** and the
3916
+ * native one is dropped with a warning (`tools` in `copilotcli-mcp.ts`).
3917
+ * - Native key of the same name means something else entirely → **refuse the
3918
+ * canonical value** rather than write a shape the tool misreads (`tools` in
3919
+ * `codexcli-mcp.ts`, where Codex reads it as a per-tool approval table).
3920
+ */
3831
3921
  enabledTools: z.optional(z.array(z.string())),
3832
3922
  disabledTools: z.optional(z.array(z.string()))
3833
3923
  });
@@ -4670,8 +4760,65 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4670
4760
  dangerouslyAllowAll: z.optional(z.boolean())
4671
4761
  });
4672
4762
  /**
4763
+ * Tool-scoped override block for Pi Coding Agent. Pi has no allow/ask/deny rule
4764
+ * surface: the only repository-syncable tool gate is `defaultTools`, the list of
4765
+ * built-in tools enabled at startup (added in v0.84.2). It is an enable-list
4766
+ * rather than a canonical allow/deny rule set, so it is authored here rather
4767
+ * than mapped onto `permission`, mirroring `KimiCodePermissionsOverrideSchema`'s
4768
+ * `tools.enabled` and `VibePermissionsOverrideSchema`'s `enabled_tools`.
4769
+ *
4770
+ * An empty array is meaningful upstream — it starts Pi with no built-in tools
4771
+ * while keeping extension and SDK custom tools — so it is emitted as written.
4772
+ * CLI flags (`--tools`, `--no-tools`, `--no-builtin-tools`, `--exclude-tools`)
4773
+ * outrank the setting, and a project array **replaces** the global one rather
4774
+ * than merging with it.
4775
+ *
4776
+ * @example
4777
+ * { "defaultTools": ["bash", "edit", "write"] }
4778
+ * @see https://pi.dev/docs/latest/settings
4779
+ */
4780
+ const PiPermissionsOverrideSchema = z.looseObject({
4781
+ permission: z.optional(ToolScopedPermissionSchema),
4782
+ defaultTools: z.optional(z.array(z.string()))
4783
+ });
4784
+ /**
4785
+ * The Antigravity CLI's four documented `toolPermission` autonomy presets.
4786
+ * Exported so the import side of `antigravity-cli-permissions.ts` filters
4787
+ * against exactly the values the enum below accepts — an unrecognized value
4788
+ * carried in verbatim would fail validation of the *whole*
4789
+ * `.rulesync/permissions.jsonc`, not just that key.
4790
+ * @see https://antigravity.google/docs/cli/reference
4791
+ */
4792
+ const ANTIGRAVITY_CLI_TOOL_PERMISSIONS = [
4793
+ "request-review",
4794
+ "proceed-in-sandbox",
4795
+ "always-proceed",
4796
+ "strict"
4797
+ ];
4798
+ /**
4799
+ * The Antigravity CLI's three documented `artifactReviewPolicy` values. Shared
4800
+ * with the import filter for the same reason as
4801
+ * {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4802
+ * @see https://antigravity.google/docs/cli/settings
4803
+ */
4804
+ const ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES = [
4805
+ "asks-for-review",
4806
+ "agent-decides",
4807
+ "always-proceed"
4808
+ ];
4809
+ /**
4810
+ * The baseline execution modes the CLI persists as `agentMode`. Shared with the
4811
+ * import filter for the same reason as {@link ANTIGRAVITY_CLI_TOOL_PERMISSIONS}.
4812
+ * @see https://antigravity.google/docs/cli/modes
4813
+ */
4814
+ const ANTIGRAVITY_CLI_AGENT_MODES = [
4815
+ "default",
4816
+ "accept-edits",
4817
+ "plan"
4818
+ ];
4819
+ /**
4673
4820
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4674
- * `settings.json` carries four global autonomy/sandbox knobs outside the
4821
+ * `settings.json` carries five global autonomy/sandbox knobs outside the
4675
4822
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4676
4823
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4677
4824
  * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
@@ -4679,7 +4826,8 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4679
4826
  * agent's artifact changes are gated on a review prompt — `asks-for-review`
4680
4827
  * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4681
4828
  * (a boolean, off by default, letting the agent read or write files outside the
4682
- * active workspace roots). Antigravity applies the allow/deny
4829
+ * active workspace roots), and `agentMode` (the baseline execution mode a session
4830
+ * starts in — `default` / `accept-edits` / `plan`). Antigravity applies the allow/deny
4683
4831
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4684
4832
  * authors these keys verbatim — no precedence modeling is needed on our side.
4685
4833
  * Fields placed here are merged onto the top level of
@@ -4687,27 +4835,27 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4687
4835
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4688
4836
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4689
4837
  * Verified against https://antigravity.google/docs/cli/reference,
4690
- * https://antigravity.google/docs/cli/sandbox and
4691
- * https://antigravity.google/docs/cli/settings.
4838
+ * https://antigravity.google/docs/cli/sandbox,
4839
+ * https://antigravity.google/docs/cli/settings and
4840
+ * https://antigravity.google/docs/cli/modes.
4692
4841
  *
4693
4842
  * @example
4694
4843
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
4695
4844
  */
4696
4845
  const AntigravityCliPermissionsOverrideSchema = z.looseObject({
4697
4846
  permission: z.optional(ToolScopedPermissionSchema),
4698
- toolPermission: z.optional(z.enum([
4699
- "request-review",
4700
- "proceed-in-sandbox",
4701
- "always-proceed",
4702
- "strict"
4703
- ])),
4847
+ toolPermission: z.optional(z.enum(ANTIGRAVITY_CLI_TOOL_PERMISSIONS)),
4704
4848
  enableTerminalSandbox: z.optional(z.boolean()),
4705
- artifactReviewPolicy: z.optional(z.enum([
4706
- "asks-for-review",
4707
- "agent-decides",
4708
- "always-proceed"
4709
- ])),
4710
- allowNonWorkspaceAccess: z.optional(z.boolean())
4849
+ artifactReviewPolicy: z.optional(z.enum(ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES)),
4850
+ allowNonWorkspaceAccess: z.optional(z.boolean()),
4851
+ /**
4852
+ * The baseline execution mode the CLI starts a session in (Agent Mode), the
4853
+ * persisted form of the `/settings` panel's Agent Mode row. Only
4854
+ * `~/.gemini/antigravity-cli/settings.json` is read for it, which is why this
4855
+ * override stays global-only.
4856
+ * @see https://antigravity.google/docs/cli/modes
4857
+ */
4858
+ agentMode: z.optional(z.enum(ANTIGRAVITY_CLI_AGENT_MODES))
4711
4859
  });
4712
4860
  /**
4713
4861
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -5017,6 +5165,7 @@ const PermissionsConfigSchema = z.looseObject({
5017
5165
  takt: z.optional(TaktPermissionsOverrideSchema),
5018
5166
  amp: z.optional(AmpPermissionsOverrideSchema),
5019
5167
  "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema),
5168
+ pi: z.optional(PiPermissionsOverrideSchema),
5020
5169
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
5021
5170
  kiro: z.optional(KiroPermissionsOverrideSchema),
5022
5171
  codexcli: z.optional(CodexcliPermissionsOverrideSchema),
@@ -5468,6 +5617,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
5468
5617
  })),
5469
5618
  cline: z.optional(z.looseObject({})),
5470
5619
  roo: z.optional(z.looseObject({})),
5620
+ amp: z.optional(z.looseObject({})),
5471
5621
  devin: z.optional(z.looseObject({
5472
5622
  "argument-hint": z.optional(z.string()),
5473
5623
  model: z.optional(z.string()),
@@ -5497,6 +5647,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
5497
5647
  cursor: z.optional(z.looseObject({
5498
5648
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
5499
5649
  "disable-model-invocation": z.optional(z.boolean()),
5650
+ "user-invocable": z.optional(z.boolean()),
5500
5651
  metadata: z.optional(z.looseObject({}))
5501
5652
  })),
5502
5653
  factorydroid: z.optional(z.looseObject({
@@ -5721,6 +5872,11 @@ async function getLocalSkillDirNames(outputRoot) {
5721
5872
  * default with a per-target value. A defined section value (including `false`)
5722
5873
  * always wins over the root default.
5723
5874
  *
5875
+ * `devin` also consumes the root-level value — it maps `true` onto a user-only
5876
+ * `triggers` list (see `devin-skill.ts`) — but has no section key of the same
5877
+ * name, so it does not go through this helper. A `devin.triggers` section value
5878
+ * still overrides it.
5879
+ *
5724
5880
  * @returns The resolved boolean, or `undefined` when neither value is set.
5725
5881
  */
5726
5882
  function resolveDisableModelInvocation({ rootFrontmatter, section }) {
@@ -5731,10 +5887,15 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
5731
5887
  *
5732
5888
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
5733
5889
  * that applies to every tool supporting the flag (claudecode, copilot,
5734
- * copilotcli, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5890
+ * copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
5735
5891
  * per-target value. A defined section value (including `false`) always wins
5736
5892
  * over the root default.
5737
5893
  *
5894
+ * `devin` also consumes the root-level value — it maps `false` onto a
5895
+ * model-only `triggers` list (see `devin-skill.ts`) — but has no section key of
5896
+ * the same name, so it does not go through this helper. A `devin.triggers`
5897
+ * section value still overrides it.
5898
+ *
5738
5899
  * @returns The resolved boolean, or `undefined` when neither value is set.
5739
5900
  */
5740
5901
  function resolveUserInvocable({ rootFrontmatter, section }) {
@@ -7584,6 +7745,22 @@ const SHARED_CONFIG_OWNERSHIP = {
7584
7745
  }
7585
7746
  }
7586
7747
  },
7748
+ ".pi/settings.json": {
7749
+ format: "json",
7750
+ invalidRootPolicy: "error",
7751
+ features: { permissions: {
7752
+ kind: "replace-owned-keys",
7753
+ ownedKeys: ["defaultTools"]
7754
+ } }
7755
+ },
7756
+ ".pi/agent/settings.json": {
7757
+ format: "json",
7758
+ invalidRootPolicy: "error",
7759
+ features: { permissions: {
7760
+ kind: "replace-owned-keys",
7761
+ ownedKeys: ["defaultTools"]
7762
+ } }
7763
+ },
7587
7764
  ".github/copilot/settings.json": {
7588
7765
  format: "json",
7589
7766
  invalidRootPolicy: "error",
@@ -11700,7 +11877,8 @@ const KiloCommandFrontmatterSchema = z.looseObject({
11700
11877
  description: z.optional(z.string()),
11701
11878
  agent: z.optional(z.string()),
11702
11879
  subtask: z.optional(z.boolean()),
11703
- model: z.optional(z.string())
11880
+ model: z.optional(z.string()),
11881
+ variant: z.optional(z.string())
11704
11882
  });
11705
11883
  var KiloCommand = class KiloCommand extends ToolCommand {
11706
11884
  frontmatter;
@@ -12169,6 +12347,8 @@ const PI_RULE_FILE_NAME = "AGENTS.md";
12169
12347
  const PI_RULE_OVERRIDE_FILE_NAME = "AGENTS.override.md";
12170
12348
  const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
12171
12349
  const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
12350
+ const PI_SETTINGS_FILE_NAME = "settings.json";
12351
+ const PI_AGENT_DIR_PATH = PI_AGENT_DIR;
12172
12352
  //#endregion
12173
12353
  //#region src/features/commands/pi-command.ts
12174
12354
  /**
@@ -14093,7 +14273,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
14093
14273
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
14094
14274
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
14095
14275
  const isEmittableString = ({ value }) => isNonEmptyString(value);
14096
- const isEmittableArray = ({ value }) => isStringArray(value);
14276
+ const isEmittableArray = ({ value }) => isStringArray$1(value);
14097
14277
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
14098
14278
  const isImportableArray = ({ value }) => isSafeStringArray(value);
14099
14279
  /**
@@ -14402,7 +14582,7 @@ function isStringRecord(value) {
14402
14582
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
14403
14583
  return Object.values(value).every((v) => typeof v === "string");
14404
14584
  }
14405
- function isStringArray(value) {
14585
+ function isStringArray$1(value) {
14406
14586
  return Array.isArray(value) && value.every((v) => typeof v === "string");
14407
14587
  }
14408
14588
  /** Compare object values without letting key order decide the answer. */
@@ -14434,7 +14614,7 @@ function isSafeStringRecord(value) {
14434
14614
  * skipped wholesale when that read fails.
14435
14615
  */
14436
14616
  function isSafeStringArray(value) {
14437
- return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14617
+ return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
14438
14618
  }
14439
14619
  /**
14440
14620
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -14476,7 +14656,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
14476
14656
  return {
14477
14657
  ...url !== void 0 && { url },
14478
14658
  ...isStringRecord(headers) && { headers },
14479
- ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14659
+ ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
14480
14660
  };
14481
14661
  }
14482
14662
  if (hookType === "mcp_tool") {
@@ -15382,6 +15562,13 @@ function generateClineHookScript({ event, commands }) {
15382
15562
  `# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
15383
15563
  `# ${CLINE_HOOK_SCRIPT_MARKER}`,
15384
15564
  "",
15565
+ "case \"${OSTYPE:-$(uname -s 2>/dev/null || true)}\" in",
15566
+ " msys*|MSYS*|cygwin*|CYGWIN*|MINGW*|mingw*)",
15567
+ ` printf '{"cancel": false, "contextModification": "", "errorMessage": ""}\\n'`,
15568
+ " exit 0",
15569
+ " ;;",
15570
+ "esac",
15571
+ "",
15385
15572
  "payload=$(cat)",
15386
15573
  "cancel=false",
15387
15574
  "error_message=''",
@@ -15395,15 +15582,39 @@ function generateClineHookScript({ event, commands }) {
15395
15582
  return lines.join("\n");
15396
15583
  }
15397
15584
  /**
15398
- * The PowerShell twin of {@link generateClineHookScript}. On Windows Cline
15399
- * resolves only `<Event>.ps1` and runs it through `powershell -File`, so both
15400
- * spellings are written and the platform picks one.
15585
+ * The PowerShell twin of {@link generateClineHookScript}. On Windows the VS Code
15586
+ * extension resolves only `<Event>.ps1` and runs it through `powershell -File`,
15587
+ * so both spellings are written and the platform picks one.
15588
+ *
15589
+ * The SDK/CLI runtime does not pick one. `listHookConfigFiles` dedupes by path,
15590
+ * so `TaskError` and `TaskError.ps1` are two entries naming the same event, and
15591
+ * `createHookCommandMap` appends both to that event's command list without a
15592
+ * per-event dedupe — it then runs every command in the list, spawning a `.ps1`
15593
+ * through `pwsh` on Unix too. That produced noise rather than a second
15594
+ * execution (the body shells out through `cmd /c`, which Unix has no such
15595
+ * thing), but it is still a failure reported on every fire.
15596
+ *
15597
+ * Hence the leading platform guard: off Windows the script answers with the
15598
+ * neutral success payload and exits, leaving the POSIX twin to do the work.
15599
+ * Its counterpart at the top of {@link generateClineHookScript} covers the
15600
+ * quadrant where the duplication is real — Windows with a POSIX shell.
15601
+ * `$IsWindows` only exists in PowerShell 6+, and is `$null` under the Windows
15602
+ * PowerShell 5.1 that `powershell -File` starts — so the guard tests that it is
15603
+ * defined *and* false, rather than `-not $IsWindows`, which would be true on
15604
+ * 5.1 and would no-op the script on the one platform it exists for.
15605
+ *
15606
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/hooks/hook-file-hooks.ts
15401
15607
  */
15402
15608
  function generateClineHookPowerShellScript({ event, commands }) {
15403
15609
  const lines = [
15404
15610
  `# ${event} hook generated by rulesync — edit .rulesync/hooks.jsonc and regenerate.`,
15405
15611
  `# ${CLINE_HOOK_SCRIPT_MARKER}`,
15406
15612
  "",
15613
+ "if ($null -ne $IsWindows -and -not $IsWindows) {",
15614
+ ` Write-Output '{"cancel": false, "contextModification": "", "errorMessage": ""}'`,
15615
+ " exit 0",
15616
+ "}",
15617
+ "",
15407
15618
  "$payload = [Console]::In.ReadToEnd()",
15408
15619
  "$cancel = $false",
15409
15620
  "$errorMessage = ''",
@@ -15428,7 +15639,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
15428
15639
  function parseManifest(fileContent) {
15429
15640
  try {
15430
15641
  const parsed = JSON.parse(fileContent);
15431
- if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
15642
+ if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
15432
15643
  return {
15433
15644
  generatedBy: "rulesync",
15434
15645
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -15465,10 +15676,14 @@ var ClineHookScript = class extends ToolFile {
15465
15676
  * by the contract, so every generated script carries a marker line and a script
15466
15677
  * without it is never overwritten.
15467
15678
  *
15468
- * Cline's CLI and SDK use a different, in-process hook surface (`AgentHooks`
15469
- * from `@cline/core`), which this adapter does not target.
15679
+ * The project hooks directory is in the search paths of both the VS Code
15680
+ * extension and the SDK/CLI, whose accepted event names differ slightly, so the
15681
+ * emitted set is their union ({@link CANONICAL_TO_CLINE_EVENT_NAMES}). Cline's
15682
+ * in-process hook surface (`AgentHooks` from `@cline/core`) is a separate
15683
+ * mechanism this adapter does not target.
15470
15684
  *
15471
15685
  * @see https://github.com/cline/cline/blob/main/apps/vscode/src/core/hooks/utils.ts
15686
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/hooks/hook-file-config.ts
15472
15687
  */
15473
15688
  var ClineHooks = class ClineHooks extends ToolHooks {
15474
15689
  scriptsByEvent;
@@ -15623,6 +15838,11 @@ const CODEXCLI_CONVERTER_CONFIG = {
15623
15838
  numberPassthroughFields: [{
15624
15839
  canonical: "additionalContextLimit",
15625
15840
  tool: "additionalContextLimit"
15841
+ }],
15842
+ booleanPassthroughFields: [{
15843
+ canonical: "async",
15844
+ tool: "async",
15845
+ commandOnly: true
15626
15846
  }]
15627
15847
  };
15628
15848
  /**
@@ -16749,6 +16969,16 @@ const FACTORYDROID_CONVERTER_CONFIG = {
16749
16969
  subdividesGroup: true
16750
16970
  }]
16751
16971
  };
16972
+ /** Droid's nine event names, the keys a standalone `hooks.json` is made of. */
16973
+ const FACTORYDROID_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_FACTORYDROID_EVENT_NAMES));
16974
+ /**
16975
+ * Whether a parsed hooks file is the standalone shape — keyed directly by event
16976
+ * name — rather than the `settings.json` shape that wraps the same map in a
16977
+ * `hooks` key.
16978
+ */
16979
+ function hasFactorydroidEventKey(parsed) {
16980
+ return Object.keys(parsed).some((key) => FACTORYDROID_EVENT_NAMES.has(key));
16981
+ }
16752
16982
  var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16753
16983
  constructor(params) {
16754
16984
  super({
@@ -16776,14 +17006,6 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16776
17006
  }
16777
17007
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
16778
17008
  const paths = FactorydroidHooks.getSettablePaths({ global });
16779
- const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16780
- const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
16781
- let settings;
16782
- try {
16783
- settings = JSON.parse(existingContent);
16784
- } catch (error) {
16785
- throw new Error(`Failed to parse existing Factory Droid hooks file at ${filePath}: ${formatError(error)}`, { cause: error });
16786
- }
16787
17009
  const config = rulesyncHooks.getJson();
16788
17010
  const factorydroidHooks = canonicalToToolHooks({
16789
17011
  config,
@@ -16791,11 +17013,7 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16791
17013
  converterConfig: FACTORYDROID_CONVERTER_CONFIG,
16792
17014
  logger
16793
17015
  });
16794
- const merged = {
16795
- ...settings,
16796
- hooks: factorydroidHooks
16797
- };
16798
- const fileContent = JSON.stringify(merged, null, 2);
17016
+ const fileContent = JSON.stringify(factorydroidHooks, null, 2);
16799
17017
  return new FactorydroidHooks({
16800
17018
  outputRoot,
16801
17019
  relativeDirPath: paths.relativeDirPath,
@@ -16805,14 +17023,14 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
16805
17023
  });
16806
17024
  }
16807
17025
  toRulesyncHooks({ logger } = {}) {
16808
- let settings;
17026
+ let parsed;
16809
17027
  try {
16810
- settings = JSON.parse(this.getFileContent());
17028
+ parsed = JSON.parse(this.getFileContent());
16811
17029
  } catch (error) {
16812
17030
  throw new Error(`Failed to parse Factory Droid hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
16813
17031
  }
16814
17032
  const hooks = toolHooksToCanonical({
16815
- hooks: settings.hooks,
17033
+ hooks: hasFactorydroidEventKey(parsed) ? parsed : parsed.hooks,
16816
17034
  converterConfig: FACTORYDROID_CONVERTER_CONFIG,
16817
17035
  logger
16818
17036
  });
@@ -16947,18 +17165,7 @@ const GROKCLI_CONVERTER_CONFIG = {
16947
17165
  tool: "env",
16948
17166
  commandOnly: true
16949
17167
  }],
16950
- noMatcherEvents: /* @__PURE__ */ new Set([
16951
- "sessionStart",
16952
- "sessionEnd",
16953
- "beforeSubmitPrompt",
16954
- "stop",
16955
- "stopFailure",
16956
- "notification",
16957
- "subagentStart",
16958
- "subagentStop",
16959
- "preCompact",
16960
- "postCompact"
16961
- ])
17168
+ noMatcherEvents: /* @__PURE__ */ new Set(["stop", "beforeSubmitPrompt"])
16962
17169
  };
16963
17170
  /**
16964
17171
  * Hooks generator for Grok CLI (xAI Grok Build).
@@ -17067,6 +17274,7 @@ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["pre_tool_call", "po
17067
17274
  const HERMESAGENT_FAIL_CLOSED_EVENT = "pre_tool_call";
17068
17275
  const HERMESAGENT_CANONICAL_EVENTS = new Set(HERMESAGENT_HOOK_EVENTS);
17069
17276
  const HERMESAGENT_NATIVE_EVENTS = new Set(HERMESAGENT_NATIVE_HOOK_EVENTS);
17277
+ const HERMESAGENT_SHELL_UNSUPPORTED_EVENTS = new Set(HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS);
17070
17278
  /**
17071
17279
  * Whether an entry of the `hooks:` mapping is a hook-event list rather than one
17072
17280
  * of its non-event siblings.
@@ -17155,7 +17363,8 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
17155
17363
  }
17156
17364
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
17157
17365
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
17158
- if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.20.0; preserving it for forward compatibility.`);
17366
+ if (HERMESAGENT_SHELL_UNSUPPORTED_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is Python-plugin-only; Hermes Agent refuses it for shell hooks, so the generated entry will never run.`);
17367
+ else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.20.2; preserving it for forward compatibility.`);
17159
17368
  setHermesHookEntries({
17160
17369
  result,
17161
17370
  event: nativeEvent,
@@ -22279,7 +22488,7 @@ function isCodexStdioServer(config) {
22279
22488
  * @see https://learn.chatgpt.com/docs/extend/mcp
22280
22489
  */
22281
22490
  function isValidRenamedArray(key, value) {
22282
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
22491
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
22283
22492
  }
22284
22493
  /**
22285
22494
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
@@ -22799,6 +23008,49 @@ const resolveCopilotcliServerType = (server) => {
22799
23008
  return isRemoteMcpServer(server) ? "http" : "stdio";
22800
23009
  };
22801
23010
  /**
23011
+ * Copilot CLI's per-server tool allowlist. `tools` selects which of the
23012
+ * server's tools are exposed — `["*"]` (the default) for all, or a list of
23013
+ * names — so the canonical `enabledTools` is written under that name rather
23014
+ * than passed through as a key the CLI ignores.
23015
+ *
23016
+ * When a server carries both, the canonical `enabledTools` wins and the native
23017
+ * `tools` is dropped with a warning. rulesync owns the generated allowlist, and
23018
+ * the `tools` value most likely present is the upstream default `["*"]` — left
23019
+ * to win, it would silently discard a real allowlist in favour of "expose every
23020
+ * tool", which is the failure this mapping exists to prevent. That is the
23021
+ * "canonical wins" branch of the collision rule documented on `enabledTools` in
23022
+ * `src/types/mcp.ts`; `kiro-mcp.ts` merges instead (its native names are
23023
+ * redundant spellings of additive lists) and `codexcli-mcp.ts` refuses instead
23024
+ * (Codex's same-named key means a different thing).
23025
+ *
23026
+ * `disabledTools` has no counterpart upstream (expressing it would need the
23027
+ * server's full tool list), so the target keeps `supportsDisabledTools: false`
23028
+ * and the processor strips it before it reaches here.
23029
+ *
23030
+ * @see https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
23031
+ */
23032
+ function renameEnabledToolsToTools({ server, serverName, logger }) {
23033
+ const { enabledTools, ...rest } = server;
23034
+ if (enabledTools === void 0) return rest;
23035
+ if (rest.tools !== void 0) logger?.warn(`[CopilotcliMcp] MCP server "${serverName}" declares both 'tools' and 'enabledTools'; keeping 'enabledTools' and dropping 'tools'.`);
23036
+ return {
23037
+ ...rest,
23038
+ tools: enabledTools
23039
+ };
23040
+ }
23041
+ /**
23042
+ * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
23043
+ * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
23044
+ * CLI-written entry can be a bare string. Canonical `enabledTools` is
23045
+ * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
23046
+ * through would reject the WHOLE imported MCP file rather than one key. Anything
23047
+ * that is not a string array is therefore left under `tools` untouched, matching
23048
+ * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
23049
+ */
23050
+ function isStringArray(value) {
23051
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
23052
+ }
23053
+ /**
22802
23054
  * Resolves and sets the transport type for each MCP server config.
22803
23055
  * GitHub Copilot CLI requires the "type" field for each server. A server it
22804
23056
  * cannot express — one that names no transport, or names one it carries no way
@@ -22842,7 +23094,11 @@ function addTypeField(mcpServers, logger) {
22842
23094
  }
22843
23095
  const { httpUrl: _httpUrl, ...rest } = parsed;
22844
23096
  result[name] = {
22845
- ...rest,
23097
+ ...renameEnabledToolsToTools({
23098
+ server: rest,
23099
+ serverName: name,
23100
+ logger
23101
+ }),
22846
23102
  type,
22847
23103
  url
22848
23104
  };
@@ -22860,7 +23116,11 @@ function addTypeField(mcpServers, logger) {
22860
23116
  continue;
22861
23117
  }
22862
23118
  result[name] = {
22863
- ...local,
23119
+ ...renameEnabledToolsToTools({
23120
+ server: local,
23121
+ serverName: name,
23122
+ logger
23123
+ }),
22864
23124
  type,
22865
23125
  command: head,
22866
23126
  ...tail.length > 0 && { args: tail }
@@ -22874,11 +23134,19 @@ function addTypeField(mcpServers, logger) {
22874
23134
  function removeTypeField(config) {
22875
23135
  const result = {};
22876
23136
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
22877
- if (server.type !== "stdio") {
22878
- result[name] = server;
23137
+ const { tools, ...withoutTools } = server;
23138
+ const restored = isStringArray(tools) ? {
23139
+ ...withoutTools,
23140
+ enabledTools: tools
23141
+ } : {
23142
+ ...withoutTools,
23143
+ ...tools !== void 0 && { tools }
23144
+ };
23145
+ if (restored.type !== "stdio") {
23146
+ result[name] = restored;
22879
23147
  continue;
22880
23148
  }
22881
- const { type: _, ...rest } = server;
23149
+ const { type: _, ...rest } = restored;
22882
23150
  result[name] = rest;
22883
23151
  }
22884
23152
  return result;
@@ -23139,6 +23407,78 @@ var CursorMcp = class CursorMcp extends ToolMcp {
23139
23407
  };
23140
23408
  //#endregion
23141
23409
  //#region src/features/mcp/deepagents-mcp.ts
23410
+ const TOOL_NAME = "deepagents";
23411
+ /**
23412
+ * Map a canonical transport onto the three dcode accepts.
23413
+ *
23414
+ * `_resolve_server_type` takes `stdio`, `sse` and `http`, plus the aliases
23415
+ * `streamable_http` / `streamable-http` → `http`. Rulesync's canonical
23416
+ * vocabulary is wider: `local` and `ws` are spellings dcode rejects outright,
23417
+ * and a rejected server is dropped at load time with only a log line. `local`
23418
+ * has an exact equivalent so it is translated; `ws` has none and is skipped at
23419
+ * generate time instead, where the warning can still reach the author.
23420
+ *
23421
+ * @see https://docs.langchain.com/oss/deepagents/code/mcp-tools
23422
+ */
23423
+ function normalizeDeepagentsTransport(transport) {
23424
+ switch (transport) {
23425
+ case "local":
23426
+ case "stdio": return "stdio";
23427
+ case "streamable-http":
23428
+ case "streamable_http":
23429
+ case "http": return "http";
23430
+ case "sse": return "sse";
23431
+ default: return;
23432
+ }
23433
+ }
23434
+ /**
23435
+ * Translate one canonical server into dcode's `.mcp.json` shape, or `null` to
23436
+ * skip it.
23437
+ *
23438
+ * Two upstream constraints from `_validate_tool_filter_fields` are enforced
23439
+ * here, because breaking either one makes dcode drop the whole server: the two
23440
+ * filters are mutually exclusive on a single server, and neither may be an
23441
+ * empty list.
23442
+ */
23443
+ function toDeepagentsServer({ name, server, logger }) {
23444
+ const rawTransport = server.transport ?? server.type;
23445
+ if (rawTransport === "ws") return warnAndSkipMcpServer({
23446
+ toolName: TOOL_NAME,
23447
+ serverName: name,
23448
+ reason: "the WebSocket transport, which deepagents does not support",
23449
+ logger
23450
+ });
23451
+ const { enabledTools, disabledTools, type: _type, transport, ...rest } = server;
23452
+ const converted = { ...rest };
23453
+ const normalized = normalizeDeepagentsTransport(rawTransport);
23454
+ if (normalized !== void 0) if (transport !== void 0) converted.transport = normalized;
23455
+ else converted.type = normalized;
23456
+ if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
23457
+ toolName: TOOL_NAME,
23458
+ serverName: name,
23459
+ reason: "both enabledTools and disabledTools, which deepagents rejects — pick one",
23460
+ logger
23461
+ });
23462
+ if (enabledTools !== void 0) {
23463
+ if (enabledTools.length === 0) return warnAndSkipMcpServer({
23464
+ toolName: TOOL_NAME,
23465
+ serverName: name,
23466
+ reason: "an empty enabledTools list, which allows no tools at all and which deepagents rejects",
23467
+ logger
23468
+ });
23469
+ converted.allowedTools = enabledTools;
23470
+ } 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.`);
23471
+ else converted.disabledTools = disabledTools;
23472
+ return converted;
23473
+ }
23474
+ /** Lift dcode's spellings back into the canonical model. */
23475
+ function toRulesyncServer(server) {
23476
+ const { allowedTools, ...rest } = server;
23477
+ const converted = { ...rest };
23478
+ for (const key of ["type", "transport"]) if (converted[key] === "streamable_http" || converted[key] === "streamable-http") converted[key] = "http";
23479
+ if (Array.isArray(allowedTools)) converted.enabledTools = allowedTools;
23480
+ return converted;
23481
+ }
23142
23482
  var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
23143
23483
  json;
23144
23484
  constructor(params) {
@@ -23173,12 +23513,22 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
23173
23513
  validate
23174
23514
  });
23175
23515
  }
23176
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
23516
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
23177
23517
  const paths = this.getSettablePaths({ global });
23178
23518
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2);
23519
+ const json = JSON.parse(fileContent);
23520
+ const mcpServers = {};
23521
+ for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
23522
+ const converted = toDeepagentsServer({
23523
+ name,
23524
+ server,
23525
+ logger
23526
+ });
23527
+ if (converted !== null) mcpServers[name] = converted;
23528
+ }
23179
23529
  const mcpJson = {
23180
- ...JSON.parse(fileContent),
23181
- mcpServers: rulesyncMcp.getMcpServers()
23530
+ ...json,
23531
+ mcpServers
23182
23532
  };
23183
23533
  return new DeepagentsMcp({
23184
23534
  outputRoot,
@@ -23189,7 +23539,9 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
23189
23539
  });
23190
23540
  }
23191
23541
  toRulesyncMcp() {
23192
- return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: this.json.mcpServers }, null, 2) });
23542
+ const servers = isRecord$1(this.json.mcpServers) ? this.json.mcpServers : {};
23543
+ const mcpServers = Object.fromEntries(Object.entries(servers).map(([name, server]) => [name, isRecord$1(server) ? toRulesyncServer(server) : server]));
23544
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
23193
23545
  }
23194
23546
  validate() {
23195
23547
  return {
@@ -23446,11 +23798,11 @@ function applyGooseStdioFields(ext, config) {
23446
23798
  if (Array.isArray(command)) {
23447
23799
  if (typeof command[0] === "string") ext.cmd = command[0];
23448
23800
  const rest = command.slice(1).filter((c) => typeof c === "string");
23449
- const args = isStringArray$1(config.args) ? config.args : [];
23801
+ const args = isStringArray$2(config.args) ? config.args : [];
23450
23802
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
23451
23803
  } else if (typeof command === "string") {
23452
23804
  ext.cmd = command;
23453
- if (isStringArray$1(config.args)) ext.args = config.args;
23805
+ if (isStringArray$2(config.args)) ext.args = config.args;
23454
23806
  }
23455
23807
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
23456
23808
  }
@@ -23552,7 +23904,7 @@ function convertFromGooseFormat(extensions) {
23552
23904
  else if (type === "streamable_http") server.type = "http";
23553
23905
  else server.type = "stdio";
23554
23906
  if (typeof ext.cmd === "string") server.command = ext.cmd;
23555
- if (isStringArray$1(ext.args)) server.args = ext.args;
23907
+ if (isStringArray$2(ext.args)) server.args = ext.args;
23556
23908
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
23557
23909
  if (typeof ext.uri === "string") server.url = ext.uri;
23558
23910
  if (isPlainObject$1(ext.headers)) server.headers = omitPrototypePollutionKeys(ext.headers);
@@ -23573,11 +23925,11 @@ function buildGoosePluginStdioServer(config) {
23573
23925
  if (Array.isArray(command)) {
23574
23926
  if (typeof command[0] === "string") server.command = command[0];
23575
23927
  const rest = command.slice(1).filter((c) => typeof c === "string");
23576
- const args = isStringArray$1(config.args) ? config.args : [];
23928
+ const args = isStringArray$2(config.args) ? config.args : [];
23577
23929
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
23578
23930
  } else if (typeof command === "string") {
23579
23931
  server.command = command;
23580
- if (isStringArray$1(config.args)) server.args = config.args;
23932
+ if (isStringArray$2(config.args)) server.args = config.args;
23581
23933
  }
23582
23934
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
23583
23935
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -23909,7 +24261,7 @@ function copyHermesOauth(source) {
23909
24261
  "client_secret"
23910
24262
  ]) if (typeof source[key] === "string") oauth[key] = source[key];
23911
24263
  if (typeof source.redirect_port === "number") oauth.redirect_port = source.redirect_port;
23912
- if (isStringArray$1(source.scopes)) oauth.scopes = source.scopes;
24264
+ if (isStringArray$2(source.scopes)) oauth.scopes = source.scopes;
23913
24265
  return Object.keys(oauth).length > 0 ? oauth : void 0;
23914
24266
  }
23915
24267
  function copyHermesAdvancedFields(source, target) {
@@ -23918,7 +24270,7 @@ function copyHermesAdvancedFields(source, target) {
23918
24270
  target.auth = source.auth;
23919
24271
  copied = true;
23920
24272
  }
23921
- if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
24273
+ if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
23922
24274
  target.client_cert = source.client_cert;
23923
24275
  copied = true;
23924
24276
  }
@@ -23987,8 +24339,8 @@ function copyHermesAdvancedFields(source, target) {
23987
24339
  */
23988
24340
  function buildHermesToolsBlock(config) {
23989
24341
  const tools = {};
23990
- if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
23991
- if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
24342
+ if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
24343
+ if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
23992
24344
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
23993
24345
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
23994
24346
  return tools;
@@ -24000,8 +24352,8 @@ function buildHermesToolsBlock(config) {
24000
24352
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
24001
24353
  */
24002
24354
  function applyHermesToolsBlock(hermesTools, server) {
24003
- if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
24004
- if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24355
+ if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
24356
+ if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
24005
24357
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
24006
24358
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
24007
24359
  }
@@ -24027,11 +24379,11 @@ function convertServerToHermes(config) {
24027
24379
  if (Array.isArray(command)) {
24028
24380
  if (typeof command[0] === "string") out.command = command[0];
24029
24381
  const rest = command.slice(1).filter((c) => typeof c === "string");
24030
- const args = isStringArray$1(config.args) ? config.args : [];
24382
+ const args = isStringArray$2(config.args) ? config.args : [];
24031
24383
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
24032
24384
  } else if (typeof command === "string") {
24033
24385
  out.command = command;
24034
- if (isStringArray$1(config.args)) out.args = config.args;
24386
+ if (isStringArray$2(config.args)) out.args = config.args;
24035
24387
  }
24036
24388
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
24037
24389
  } else if (url !== void 0) {
@@ -24081,7 +24433,7 @@ function convertFromHermesFormat(mcpServers) {
24081
24433
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
24082
24434
  const server = {};
24083
24435
  if (typeof config.command === "string") server.command = config.command;
24084
- if (isStringArray$1(config.args)) server.args = config.args;
24436
+ if (isStringArray$2(config.args)) server.args = config.args;
24085
24437
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
24086
24438
  if (typeof config.url === "string") server.url = config.url;
24087
24439
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -25039,13 +25391,17 @@ function mergeToolLists(...lists) {
25039
25391
  *
25040
25392
  * Both native names are documented per-server fields, so a config that already
25041
25393
  * spells them natively keeps working: the two lists are merged rather than
25042
- * one overwriting the other.
25394
+ * one overwriting the other. That is the "merge" branch of the collision rule
25395
+ * documented on `enabledTools` in `src/types/mcp.ts` — it applies here because
25396
+ * the native names are redundant spellings of the same additive lists;
25397
+ * `copilotcli-mcp.ts` lets the canonical value win instead, and
25398
+ * `codexcli-mcp.ts` refuses the canonical value instead.
25043
25399
  * @see https://kiro.dev/docs/mcp/configuration/
25044
25400
  */
25045
25401
  function toKiroMcpServers(servers) {
25046
25402
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25047
25403
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
25048
- const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25404
+ const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
25049
25405
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
25050
25406
  return [name, {
25051
25407
  ...rest,
@@ -25069,7 +25425,7 @@ function fromKiroMcpServers(servers) {
25069
25425
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
25070
25426
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
25071
25427
  const { autoApprove, ...rest } = server;
25072
- if (!isStringArray$1(autoApprove)) return [name, server];
25428
+ if (!isStringArray$2(autoApprove)) return [name, server];
25073
25429
  return [name, {
25074
25430
  ...rest,
25075
25431
  kiroAutoApprove: autoApprove
@@ -26211,7 +26567,7 @@ async function readRovodevConfigYaml({ outputRoot }) {
26211
26567
  }
26212
26568
  function disabledNamesOf(config) {
26213
26569
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
26214
- return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26570
+ return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
26215
26571
  }
26216
26572
  /**
26217
26573
  * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
@@ -26344,7 +26700,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
26344
26700
  return [];
26345
26701
  }
26346
26702
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
26347
- const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26703
+ const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
26348
26704
  const managedNameSet = new Set(managedNames);
26349
26705
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
26350
26706
  if (reEnabled.length > 0) logger?.warn(`Rovo Dev MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
@@ -27186,7 +27542,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27186
27542
  meta: {
27187
27543
  supportsProject: true,
27188
27544
  supportsGlobal: true,
27189
- supportsEnabledTools: false,
27545
+ supportsEnabledTools: true,
27190
27546
  supportsDisabledTools: false
27191
27547
  }
27192
27548
  }],
@@ -27204,8 +27560,8 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27204
27560
  meta: {
27205
27561
  supportsProject: true,
27206
27562
  supportsGlobal: true,
27207
- supportsEnabledTools: false,
27208
- supportsDisabledTools: false
27563
+ supportsEnabledTools: true,
27564
+ supportsDisabledTools: true
27209
27565
  }
27210
27566
  }],
27211
27567
  ["factorydroid", {
@@ -27214,7 +27570,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27214
27570
  supportsProject: true,
27215
27571
  supportsGlobal: true,
27216
27572
  supportsEnabledTools: false,
27217
- supportsDisabledTools: false
27573
+ supportsDisabledTools: true
27218
27574
  }
27219
27575
  }],
27220
27576
  ["goose", {
@@ -27340,7 +27696,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27340
27696
  supportsProject: true,
27341
27697
  supportsGlobal: false,
27342
27698
  supportsEnabledTools: false,
27343
- supportsDisabledTools: false
27699
+ supportsDisabledTools: true
27344
27700
  }
27345
27701
  }],
27346
27702
  ["zoocode", {
@@ -27349,7 +27705,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
27349
27705
  supportsProject: true,
27350
27706
  supportsGlobal: false,
27351
27707
  supportsEnabledTools: false,
27352
- supportsDisabledTools: false
27708
+ supportsDisabledTools: true
27353
27709
  }
27354
27710
  }],
27355
27711
  ["rovodev", {
@@ -28010,6 +28366,34 @@ function convertAmpToRulesync({ disable, permissions }) {
28010
28366
  //#endregion
28011
28367
  //#region src/features/permissions/antigravity-cli-permissions.ts
28012
28368
  /**
28369
+ * Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
28370
+ * permissions override may author. They are siblings of `permissions`, passed
28371
+ * through verbatim in both directions.
28372
+ */
28373
+ const ANTIGRAVITY_CLI_OVERRIDE_KEYS = [
28374
+ "toolPermission",
28375
+ "enableTerminalSandbox",
28376
+ "artifactReviewPolicy",
28377
+ "allowNonWorkspaceAccess",
28378
+ "agentMode"
28379
+ ];
28380
+ const moduleLogger$2 = fallbackLogger;
28381
+ /**
28382
+ * Narrow an imported `settings.json` value to one of the documented values of
28383
+ * an enum-typed override key. The override schema declares `toolPermission`,
28384
+ * `artifactReviewPolicy` and `agentMode` as `z.enum`, so carrying an
28385
+ * unrecognized value through — a hand-written typo, or a preset a newer
28386
+ * Antigravity release added — would fail validation of the whole imported
28387
+ * `.rulesync/permissions.jsonc` instead of dropping just that key. A dropped
28388
+ * value is warned about rather than discarded silently, so a preset rulesync
28389
+ * does not know about yet is visible instead of quietly missing.
28390
+ */
28391
+ function pickDocumentedValue({ key, value, documented }) {
28392
+ if (typeof value !== "string") return void 0;
28393
+ if (documented.includes(value)) return value;
28394
+ moduleLogger$2.warn(`Antigravity CLI permissions: dropping '${key}: ${value}' on import — it is not one of the documented values (${documented.join(", ")}), and carrying it through would fail validation of the whole permissions file. Author it under the 'antigravity-cli' override by hand once rulesync supports it.`);
28395
+ }
28396
+ /**
28013
28397
  * Deliberate mapping from rulesync canonical permission categories to the
28014
28398
  * Antigravity CLI Fine-Grained Permissions Engine action vocabulary
28015
28399
  * (`read_file`, `write_file`, `read_url`, `command`, `mcp`, plus `execute_url` /
@@ -28084,9 +28468,10 @@ function buildPermissionEntry$1(toolName, pattern) {
28084
28468
  * file (global scope only). The file holds other CLI settings besides
28085
28469
  * permissions, so it is never deleted.
28086
28470
  *
28087
- * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28471
+ * Five CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
28088
28472
  * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
28089
- * `artifactReviewPolicy` and `allowNonWorkspaceAccess` — are authored and
28473
+ * `artifactReviewPolicy`, `allowNonWorkspaceAccess` and `agentMode` (the
28474
+ * baseline execution mode) — are authored and
28090
28475
  * round-tripped through the `antigravity-cli` permissions override (see
28091
28476
  * `AntigravityCliPermissionsOverrideSchema`).
28092
28477
  */
@@ -28150,10 +28535,10 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28150
28535
  permissions: mergedPermissions
28151
28536
  };
28152
28537
  const override = config["antigravity-cli"];
28153
- if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
28154
- if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
28155
- if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
28156
- if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
28538
+ for (const key of ANTIGRAVITY_CLI_OVERRIDE_KEYS) {
28539
+ const value = override?.[key];
28540
+ if (value !== void 0) merged[key] = value;
28541
+ }
28157
28542
  const fileContent = JSON.stringify(merged, null, 2);
28158
28543
  return new AntigravityCliPermissions({
28159
28544
  outputRoot,
@@ -28178,10 +28563,26 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
28178
28563
  deny: permissions.deny ?? []
28179
28564
  });
28180
28565
  const override = {};
28181
- if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
28566
+ const toolPermission = pickDocumentedValue({
28567
+ key: "toolPermission",
28568
+ value: settings.toolPermission,
28569
+ documented: ANTIGRAVITY_CLI_TOOL_PERMISSIONS
28570
+ });
28571
+ if (toolPermission !== void 0) override.toolPermission = toolPermission;
28182
28572
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
28183
- if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
28573
+ const artifactReviewPolicy = pickDocumentedValue({
28574
+ key: "artifactReviewPolicy",
28575
+ value: settings.artifactReviewPolicy,
28576
+ documented: ANTIGRAVITY_CLI_ARTIFACT_REVIEW_POLICIES
28577
+ });
28578
+ if (artifactReviewPolicy !== void 0) override.artifactReviewPolicy = artifactReviewPolicy;
28184
28579
  if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
28580
+ const agentMode = pickDocumentedValue({
28581
+ key: "agentMode",
28582
+ value: settings.agentMode,
28583
+ documented: ANTIGRAVITY_CLI_AGENT_MODES
28584
+ });
28585
+ if (agentMode !== void 0) override.agentMode = agentMode;
28185
28586
  const result = { ...config };
28186
28587
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
28187
28588
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -29007,6 +29408,119 @@ function deepMergeRecords(base, patch) {
29007
29408
  }
29008
29409
  return merged;
29009
29410
  }
29411
+ /**
29412
+ * `sandbox.*` paths Claude Code honors only from user settings, managed settings
29413
+ * and the `--settings` CLI flag. Written into a project `.claude/settings.json`
29414
+ * they are silently ignored, so a committed file would read as though it
29415
+ * enforced a sandbox policy while doing nothing — security-relevant for
29416
+ * `network.strictAllowlist` and the credential-masking keys in particular.
29417
+ * Generation therefore drops them at project scope with a per-key warning and
29418
+ * emits them only under `--global`.
29419
+ *
29420
+ * Deliberately NOT listed:
29421
+ * - `bwrapPath` / `socatPath`: v2.1.232 added them to the managed-settings
29422
+ * approval dialog, which is a consent prompt, not a project-scope rejection.
29423
+ * - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
29424
+ * unit is the individual entry's mode, not the settings key, and the same
29425
+ * lists carry `deny` entries that project settings *do* honor — dropping a
29426
+ * whole list would remove real restrictions. `stripProjectIgnoredMaskEntries`
29427
+ * filters those lists per entry instead.
29428
+ *
29429
+ * @see https://code.claude.com/docs/en/sandboxing
29430
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
29431
+ */
29432
+ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
29433
+ ["filesystem", "disabled"],
29434
+ ["network", "strictAllowlist"],
29435
+ ["network", "tlsTerminate"],
29436
+ ["credentials", "allowPlaintextInject"],
29437
+ ["credentials", "awsPairs"],
29438
+ ["credentials", "sigv4"],
29439
+ ["allowAppleEvents"],
29440
+ ["ripgrep"]
29441
+ ];
29442
+ /**
29443
+ * Copy of the authored `sandbox` override with the user/managed-only paths
29444
+ * removed, warning once per dropped path. Only the override copy is filtered —
29445
+ * a value already hand-written in the target file is left untouched, matching
29446
+ * the `qwencode` `security.allowPrivateNetworkHooks` precedent.
29447
+ */
29448
+ function stripGlobalOnlySandboxPaths({ sandbox, relativeFilePath, logger }) {
29449
+ const filtered = structuredClone(sandbox);
29450
+ for (const path of CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS) {
29451
+ const leaf = path.at(-1);
29452
+ if (leaf === void 0) continue;
29453
+ const parentPath = path.slice(0, -1);
29454
+ let parent = filtered;
29455
+ for (const segment of parentPath) {
29456
+ const next = parent[segment];
29457
+ if (!isPlainRecord(next)) {
29458
+ parent = {};
29459
+ break;
29460
+ }
29461
+ parent = next;
29462
+ }
29463
+ if (parent[leaf] === void 0) continue;
29464
+ delete parent[leaf];
29465
+ const [container] = parentPath;
29466
+ if (container !== void 0 && isPlainRecord(filtered[container])) {
29467
+ if (Object.keys(filtered[container]).length === 0) delete filtered[container];
29468
+ }
29469
+ logger?.warn(`Claude Code permissions: 'sandbox.${path.join(".")}' is only honored in user/managed/--settings settings, so it is not written to the project-scoped ${relativeFilePath}. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
29470
+ }
29471
+ return filtered;
29472
+ }
29473
+ /** The `sandbox.credentials` lists whose entries accept `"mode": "mask"`. */
29474
+ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
29475
+ /**
29476
+ * Copy of the authored `sandbox` override with the `credentials.envVars` /
29477
+ * `credentials.files` entries whose `mode` is `"mask"` removed, warning once per
29478
+ * list.
29479
+ *
29480
+ * Masking authorizes the sandbox proxy to send the real credential to the listed
29481
+ * hosts, so Claude Code honors it only from user settings, managed settings and
29482
+ * the `--settings` CLI flag; a `mask` entry in a repository's
29483
+ * `.claude/settings.json` is ignored outright. Keeping it would read as though
29484
+ * the credential were masked while nothing protects it — the one project-scope
29485
+ * gap whose consequence is a live credential leaving the sandbox unmasked.
29486
+ *
29487
+ * Filtered per entry rather than per key, because the same lists carry `deny`
29488
+ * entries that project settings *do* honor. `mode` is matched exactly: an entry
29489
+ * Claude Code cannot read as `mask` is not treated as one either (it degrades
29490
+ * such entries to `deny`), so the "reads as masked but isn't" state this guards
29491
+ * against cannot slip through a differently-spelled value.
29492
+ *
29493
+ * Like `stripGlobalOnlySandboxPaths`, only the override copy is filtered — a
29494
+ * value already in the target file is left untouched, which is why the warning
29495
+ * points at it.
29496
+ *
29497
+ * @see https://code.claude.com/docs/en/sandboxing — "`mask` entries … are all
29498
+ * ignored in a repository's `.claude/settings.json` or
29499
+ * `.claude/settings.local.json`"; the file-entry section states the same
29500
+ * settings-source restriction applies there.
29501
+ */
29502
+ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
29503
+ const credentials = sandbox.credentials;
29504
+ if (!isPlainRecord(credentials)) return sandbox;
29505
+ const filteredCredentials = { ...credentials };
29506
+ let changed = false;
29507
+ for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
29508
+ const list = filteredCredentials[listKey];
29509
+ if (!Array.isArray(list)) continue;
29510
+ const kept = list.filter((entry) => !(isPlainRecord(entry) && entry.mode === "mask"));
29511
+ if (kept.length === list.length) continue;
29512
+ changed = true;
29513
+ const dropped = list.length - kept.length;
29514
+ logger?.warn(`Claude Code permissions: 'sandbox.credentials.${listKey}' entries with 'mode: "mask"' are only honored in user/managed/--settings settings, so ${dropped} of them ${dropped === 1 ? "was" : "were"} not written to the project-scoped ${relativeFilePath}. Author them in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
29515
+ if (kept.length === 0) delete filteredCredentials[listKey];
29516
+ else filteredCredentials[listKey] = kept;
29517
+ }
29518
+ if (!changed) return sandbox;
29519
+ const filtered = { ...sandbox };
29520
+ if (Object.keys(filteredCredentials).length === 0) delete filtered.credentials;
29521
+ else filtered.credentials = filteredCredentials;
29522
+ return filtered;
29523
+ }
29010
29524
  const CLAUDE_PATH_RULE_ALIASES = {
29011
29525
  Write: "Edit",
29012
29526
  NotebookEdit: "Edit",
@@ -29060,7 +29574,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29060
29574
  validate
29061
29575
  });
29062
29576
  }
29063
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
29577
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
29064
29578
  const paths = ClaudecodePermissions.getSettablePaths();
29065
29579
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
29066
29580
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -29084,7 +29598,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
29084
29598
  };
29085
29599
  }
29086
29600
  const overrideSandbox = config.claudecode?.sandbox;
29087
- if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
29601
+ if (isPlainRecord(overrideSandbox)) {
29602
+ const scopedSandbox = global ? overrideSandbox : stripProjectIgnoredMaskEntries({
29603
+ sandbox: stripGlobalOnlySandboxPaths({
29604
+ sandbox: overrideSandbox,
29605
+ relativeFilePath: paths.relativeFilePath,
29606
+ logger
29607
+ }),
29608
+ relativeFilePath: paths.relativeFilePath,
29609
+ logger
29610
+ });
29611
+ if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
29612
+ }
29088
29613
  const managedToolNames = managedClaudeToolNames(config);
29089
29614
  const merged = applyPermissions({
29090
29615
  settings,
@@ -31261,7 +31786,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
31261
31786
  function convertGoosePermissionConfigToRulesync(userPermission) {
31262
31787
  const permission = {};
31263
31788
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
31264
- const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
31789
+ const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
31265
31790
  const action = GOOSE_LIST_TO_ACTION[key];
31266
31791
  for (const toolName of toolNames) {
31267
31792
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
@@ -31276,6 +31801,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
31276
31801
  const GROKCLI_UI_KEY = "ui";
31277
31802
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
31278
31803
  const GROKCLI_PERMISSION_KEY = "permission";
31804
+ const GROKCLI_AUTO_PERMISSION_MODE = "auto";
31279
31805
  const CATCH_ALL_PATTERN$2 = "*";
31280
31806
  const MCP_CANONICAL_PREFIX$1 = "mcp__";
31281
31807
  const CATEGORY_TO_GROK_TOOL = {
@@ -31489,8 +32015,9 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
31489
32015
  deny: buckets.deny,
31490
32016
  ask: buckets.ask
31491
32017
  };
31492
- const uiPatch = global ? { [GROKCLI_UI_KEY]: {
31493
- ...isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
32018
+ const existingUi = isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
32019
+ const uiPatch = global && existingUi[GROKCLI_PERMISSION_MODE_KEY] !== GROKCLI_AUTO_PERMISSION_MODE ? { [GROKCLI_UI_KEY]: {
32020
+ ...existingUi,
31494
32021
  [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
31495
32022
  } } : {};
31496
32023
  return new GrokcliPermissions({
@@ -31552,7 +32079,7 @@ const ACTION_RANK = {
31552
32079
  * (mirrors the Cursor adapter's preservation of unmanaged types).
31553
32080
  */
31554
32081
  function unmanagedEntries(existingPermission, key) {
31555
- return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
32082
+ return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
31556
32083
  }
31557
32084
  /**
31558
32085
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -31595,9 +32122,9 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
31595
32122
  * described more than once resolves to the strictest action.
31596
32123
  */
31597
32124
  function parseGrokPermissionArrays(permission) {
31598
- const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
31599
- const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
31600
- const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
32125
+ const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
32126
+ const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
32127
+ const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
31601
32128
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
31602
32129
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
31603
32130
  const result = {};
@@ -31779,20 +32306,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
31779
32306
  const permission = clonePermissionBlock(provenance.permission);
31780
32307
  reconcileCommandAllowlist({
31781
32308
  permission,
31782
- commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
32309
+ commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
31783
32310
  });
31784
32311
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
31785
32312
  reconcileNativeDenies({
31786
32313
  permission,
31787
32314
  category: "bash",
31788
- patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
32315
+ patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
31789
32316
  });
31790
32317
  const security = isRecord$1(config.security) ? config.security : {};
31791
32318
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
31792
32319
  reconcileNativeDenies({
31793
32320
  permission,
31794
32321
  category: "webfetch",
31795
- patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
32322
+ patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
31796
32323
  });
31797
32324
  removeEmptyCategories(permission);
31798
32325
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -33220,6 +33747,115 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
33220
33747
  }
33221
33748
  };
33222
33749
  //#endregion
33750
+ //#region src/features/permissions/pi-permissions.ts
33751
+ /** The one `settings.json` key this feature owns. */
33752
+ const DEFAULT_TOOLS_KEY = "defaultTools";
33753
+ /**
33754
+ * Permissions generator for Pi Coding Agent.
33755
+ *
33756
+ * Pi exposes no allow/ask/deny rule surface, so no canonical permission
33757
+ * category maps onto it. Its one repository-syncable tool gate is `defaultTools` —
33758
+ * the built-in tools enabled at startup (v0.84.2) — authored through the `pi`
33759
+ * permissions override.
33760
+ *
33761
+ * Scope semantics are the **opposite** of the union-merge most targets use:
33762
+ * a project `defaultTools` array *replaces* the global array rather than adding
33763
+ * to it, so the two scopes are written independently and never combined.
33764
+ * Project scope writes `.pi/settings.json`; global scope writes
33765
+ * `~/.pi/agent/settings.json`.
33766
+ *
33767
+ * The file is hand-edited and holds many unrelated keys (`theme`,
33768
+ * `defaultModel`, `packages`, `sessionDir`, ...), so writes go through the
33769
+ * shared-config gateway with `defaultTools` as the only owned key, and the file
33770
+ * is never deleted.
33771
+ *
33772
+ * @see https://pi.dev/docs/latest/settings
33773
+ */
33774
+ var PiPermissions = class PiPermissions extends ToolPermissions {
33775
+ constructor(params) {
33776
+ super({
33777
+ ...params,
33778
+ fileContent: params.fileContent ?? "{}"
33779
+ });
33780
+ }
33781
+ /** `settings.json` holds unrelated user settings, so it must not be deleted. */
33782
+ isDeletable() {
33783
+ return false;
33784
+ }
33785
+ static getSettablePaths({ global = false } = {}) {
33786
+ return {
33787
+ relativeDirPath: global ? PI_AGENT_DIR_PATH : ".pi",
33788
+ relativeFilePath: PI_SETTINGS_FILE_NAME
33789
+ };
33790
+ }
33791
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33792
+ const paths = PiPermissions.getSettablePaths({ global });
33793
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
33794
+ return new PiPermissions({
33795
+ outputRoot,
33796
+ relativeDirPath: paths.relativeDirPath,
33797
+ relativeFilePath: paths.relativeFilePath,
33798
+ fileContent,
33799
+ validate,
33800
+ global
33801
+ });
33802
+ }
33803
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
33804
+ const paths = PiPermissions.getSettablePaths({ global });
33805
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
33806
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
33807
+ const defaultTools = rulesyncPermissions.getJson().pi?.defaultTools;
33808
+ const patch = {};
33809
+ if (defaultTools !== void 0) patch[DEFAULT_TOOLS_KEY] = defaultTools;
33810
+ return new PiPermissions({
33811
+ outputRoot,
33812
+ relativeDirPath: paths.relativeDirPath,
33813
+ relativeFilePath: paths.relativeFilePath,
33814
+ fileContent: applySharedConfigPatch({
33815
+ fileKey: sharedConfigFileKey(paths),
33816
+ feature: "permissions",
33817
+ existingContent,
33818
+ patch,
33819
+ filePath
33820
+ }),
33821
+ validate: true,
33822
+ global
33823
+ });
33824
+ }
33825
+ toRulesyncPermissions() {
33826
+ const defaultTools = this.parseSettings()[DEFAULT_TOOLS_KEY];
33827
+ const config = { permission: {} };
33828
+ if (Array.isArray(defaultTools) && defaultTools.every((tool) => typeof tool === "string")) config.pi = { [DEFAULT_TOOLS_KEY]: defaultTools };
33829
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
33830
+ }
33831
+ parseSettings() {
33832
+ try {
33833
+ return parseSharedConfig({
33834
+ format: "json",
33835
+ fileContent: this.getFileContent()
33836
+ });
33837
+ } catch (error) {
33838
+ throw new Error(`Failed to parse Pi settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
33839
+ }
33840
+ }
33841
+ validate() {
33842
+ return {
33843
+ success: true,
33844
+ error: null
33845
+ };
33846
+ }
33847
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
33848
+ return new PiPermissions({
33849
+ outputRoot,
33850
+ relativeDirPath,
33851
+ relativeFilePath,
33852
+ fileContent: "{}",
33853
+ validate: false,
33854
+ global
33855
+ });
33856
+ }
33857
+ };
33858
+ //#endregion
33223
33859
  //#region src/features/permissions/qwencode-permissions.ts
33224
33860
  /**
33225
33861
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -34283,7 +34919,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
34283
34919
  permission[category] ??= {};
34284
34920
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
34285
34921
  }
34286
- if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34922
+ if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
34287
34923
  permission.read ??= {};
34288
34924
  permission.read[path] ??= "allow";
34289
34925
  }
@@ -35260,8 +35896,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
35260
35896
  const executionProfiles = isRecord$1(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
35261
35897
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
35262
35898
  const config = convertWarpToRulesyncPermissions({
35263
- allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35264
- deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35899
+ allow: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
35900
+ deny: defaultProfile ? isStringArray$2(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
35265
35901
  });
35266
35902
  const warpOverride = {};
35267
35903
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -35973,6 +36609,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
35973
36609
  supportsImport: true
35974
36610
  }
35975
36611
  }],
36612
+ ["pi", {
36613
+ class: PiPermissions,
36614
+ meta: {
36615
+ supportsProject: true,
36616
+ supportsGlobal: true,
36617
+ supportsImport: true
36618
+ }
36619
+ }],
35976
36620
  ["qwencode", {
35977
36621
  class: QwencodePermissions,
35978
36622
  meta: {
@@ -36775,11 +37419,12 @@ var AmpSkill = class AmpSkill extends ToolSkill {
36775
37419
  };
36776
37420
  }
36777
37421
  toRulesyncSkill() {
36778
- const frontmatter = this.getFrontmatter();
37422
+ const { name, description, ...ampSection } = this.getFrontmatter();
36779
37423
  const rulesyncFrontmatter = {
36780
- name: frontmatter.name,
36781
- description: frontmatter.description,
36782
- targets: ["*"]
37424
+ name,
37425
+ description,
37426
+ targets: ["*"],
37427
+ ...Object.keys(ampSection).length > 0 && { amp: ampSection }
36783
37428
  };
36784
37429
  return new RulesyncSkill({
36785
37430
  outputRoot: this.outputRoot,
@@ -36796,6 +37441,7 @@ var AmpSkill = class AmpSkill extends ToolSkill {
36796
37441
  const settablePaths = AmpSkill.getSettablePaths({ global });
36797
37442
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
36798
37443
  const ampFrontmatter = {
37444
+ ...rulesyncFrontmatter.amp,
36799
37445
  name: rulesyncFrontmatter.name,
36800
37446
  description: rulesyncFrontmatter.description
36801
37447
  };
@@ -38282,6 +38928,7 @@ const CursorSkillFrontmatterSchema = z.looseObject({
38282
38928
  description: z.string(),
38283
38929
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
38284
38930
  "disable-model-invocation": z.optional(z.boolean()),
38931
+ "user-invocable": z.optional(z.boolean()),
38285
38932
  metadata: z.optional(z.looseObject({}))
38286
38933
  });
38287
38934
  /**
@@ -38336,6 +38983,7 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38336
38983
  const cursorSection = {
38337
38984
  ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
38338
38985
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
38986
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
38339
38987
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
38340
38988
  };
38341
38989
  const rulesyncFrontmatter = {
@@ -38363,11 +39011,16 @@ var CursorSkill = class CursorSkill extends ToolSkill {
38363
39011
  rootFrontmatter: rulesyncFrontmatter,
38364
39012
  section: cursorSection
38365
39013
  });
39014
+ const resolvedUserInvocable = resolveUserInvocable({
39015
+ rootFrontmatter: rulesyncFrontmatter,
39016
+ section: cursorSection
39017
+ });
38366
39018
  const cursorFrontmatter = {
38367
39019
  name: rulesyncFrontmatter.name,
38368
39020
  description: rulesyncFrontmatter.description,
38369
39021
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
38370
39022
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
39023
+ ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
38371
39024
  ...cursorSection?.metadata !== void 0 && { metadata: cursorSection.metadata }
38372
39025
  };
38373
39026
  return new CursorSkill({
@@ -39222,6 +39875,47 @@ const JunieSkillFrontmatterSchema = z.looseObject({
39222
39875
  name: z.string(),
39223
39876
  description: z.string()
39224
39877
  });
39878
+ /** An ATX markdown heading line (`#` … `######`). */
39879
+ const HEADING_LINE = /^#{1,6}(\s|$)/;
39880
+ /**
39881
+ * Junie's own fallback for a `SKILL.md` with no `description`: "If
39882
+ * `description` is not provided in the frontmatter, Junie CLI extracts the
39883
+ * first paragraph of the body content as the description." Headings do not
39884
+ * count as that paragraph — "If the body is also empty or contains only
39885
+ * headings, the skill will fail to load."
39886
+ *
39887
+ * This is import-only. The canonical `RulesyncSkillFrontmatter` requires a
39888
+ * description, so without the fallback a skill Junie itself loads fine aborts
39889
+ * the whole import; generation keeps emitting an explicit description, which
39890
+ * the same docs recommend.
39891
+ *
39892
+ * Heading lines are therefore skipped rather than taken: a body opening with
39893
+ * `# Skill Name` would otherwise import that title as the description and —
39894
+ * because the next generate writes it out explicitly — replace Junie's own
39895
+ * correct fallback with the wrong value everywhere, canonical config included.
39896
+ *
39897
+ * A paragraph runs to the first blank line or heading, and is collapsed onto
39898
+ * one line because it becomes a YAML frontmatter value. A fenced code block is
39899
+ * not treated specially: it is ordinary content, so a body whose first
39900
+ * paragraph is a fence yields the fence text. Returns an empty string when the
39901
+ * body holds no such paragraph, which the caller turns into a skipped skill.
39902
+ *
39903
+ * @see https://junie.jetbrains.com/docs/agent-skills.html
39904
+ */
39905
+ function deriveDescriptionFromBody(body) {
39906
+ const paragraph = [];
39907
+ for (const rawLine of body.split(/\r?\n/)) {
39908
+ const line = rawLine.trim();
39909
+ if (paragraph.length === 0) {
39910
+ if (line === "" || HEADING_LINE.test(line)) continue;
39911
+ paragraph.push(line);
39912
+ continue;
39913
+ }
39914
+ if (line === "" || HEADING_LINE.test(line)) break;
39915
+ paragraph.push(line);
39916
+ }
39917
+ return paragraph.join(" ").trim();
39918
+ }
39225
39919
  /**
39226
39920
  * Represents a JetBrains Junie skill directory.
39227
39921
  * Skills are stored under the .junie/skills directory with SKILL.md files.
@@ -39318,7 +40012,16 @@ var JunieSkill = class JunieSkill extends ToolSkill {
39318
40012
  ...params,
39319
40013
  getSettablePaths: JunieSkill.getSettablePaths
39320
40014
  });
39321
- const result = JunieSkillFrontmatterSchema.safeParse(loaded.frontmatter);
40015
+ let frontmatter = loaded.frontmatter;
40016
+ if (isRecord$1(frontmatter) && frontmatter.description === void 0) {
40017
+ const derived = deriveDescriptionFromBody(loaded.body);
40018
+ if (derived === "") throw new Error(`Cannot import ${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.`);
40019
+ frontmatter = {
40020
+ ...frontmatter,
40021
+ description: derived
40022
+ };
40023
+ }
40024
+ const result = JunieSkillFrontmatterSchema.safeParse(frontmatter);
39322
40025
  if (!result.success) {
39323
40026
  const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
39324
40027
  throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
@@ -41783,7 +42486,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
41783
42486
  meta: {
41784
42487
  supportsProject: true,
41785
42488
  supportsSimulated: false,
41786
- supportsGlobal: true
42489
+ supportsGlobal: true,
42490
+ lenientImport: true
41787
42491
  }
41788
42492
  }],
41789
42493
  ["kilo", {
@@ -46319,6 +47023,23 @@ var TaktSubagent = class TaktSubagent extends ToolSubagent {
46319
47023
  };
46320
47024
  //#endregion
46321
47025
  //#region src/features/subagents/vibe-subagent.ts
47026
+ /**
47027
+ * Where Vibe reads a custom system prompt from: `.vibe/prompts/<id>.md`
47028
+ * (project) and `~/.vibe/prompts/<id>.md` (user), with the custom directories
47029
+ * winning over the builtins. The id must be a bare file name with no path
47030
+ * separators.
47031
+ * @see vibe/core/prompts/__init__.py
47032
+ */
47033
+ const VIBE_PROMPTS_DIR_PATH = join(".vibe", "prompts");
47034
+ /**
47035
+ * Vibe resolves a `system_prompt_id` by file name, and
47036
+ * `VibeConfigSchema._check_system_prompt` evaluates it during validation — an
47037
+ * unresolvable id raises and the agent is dropped at discovery. The id must
47038
+ * therefore stay a bare slug with no path separators.
47039
+ */
47040
+ function promptSlug(relativeFilePath) {
47041
+ return basename(relativeFilePath).replace(/\.(md|toml)$/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
47042
+ }
46322
47043
  const VibeSubagentTomlSchema = z.looseObject({
46323
47044
  agent_type: z.enum(["agent", "subagent"]),
46324
47045
  display_name: z.optional(z.string()),
@@ -46335,14 +47056,22 @@ const VibeSubagentTomlSchema = z.looseObject({
46335
47056
  });
46336
47057
  var VibeSubagent = class VibeSubagent extends ToolSubagent {
46337
47058
  body;
46338
- constructor({ body, ...rest }) {
46339
- if (rest.validate !== false) try {
47059
+ promptFile;
47060
+ resolvedSystemPrompt;
47061
+ constructor({ body, promptFile = false, resolvedSystemPrompt, ...rest }) {
47062
+ if (rest.validate !== false && !promptFile) try {
46340
47063
  VibeSubagentTomlSchema.parse(smolToml.parse(body));
46341
47064
  } catch (error) {
46342
47065
  throw new Error(`Invalid TOML in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(error)}`, { cause: error });
46343
47066
  }
46344
47067
  super({ ...rest });
46345
47068
  this.body = body;
47069
+ this.promptFile = promptFile;
47070
+ this.resolvedSystemPrompt = resolvedSystemPrompt;
47071
+ }
47072
+ /** True for the companion `.vibe/prompts/<id>.md` file. */
47073
+ isPromptFile() {
47074
+ return this.promptFile;
46346
47075
  }
46347
47076
  static getSettablePaths(_options = {}) {
46348
47077
  return { relativeDirPath: join(".vibe", "agents") };
@@ -46357,7 +47086,9 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46357
47086
  } catch (error) {
46358
47087
  throw new Error(`Failed to parse TOML in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
46359
47088
  }
46360
- const { system_prompt, description, display_name, ...vibeSection } = parsed;
47089
+ const { system_prompt, system_prompt_id, description, display_name, ...vibeSection } = parsed;
47090
+ const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
47091
+ const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
46361
47092
  const fileStem = basename(this.getRelativeFilePath(), ".toml");
46362
47093
  const rulesyncFrontmatter = {
46363
47094
  targets: ["vibe"],
@@ -46366,13 +47097,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46366
47097
  vibe: {
46367
47098
  ...display_name !== void 0 && { display_name },
46368
47099
  ...description !== void 0 && { description },
47100
+ ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
46369
47101
  ...vibeSection
46370
47102
  }
46371
47103
  };
46372
47104
  return new RulesyncSubagent({
46373
47105
  outputRoot: this.outputRoot,
46374
47106
  frontmatter: rulesyncFrontmatter,
46375
- body: system_prompt ?? "",
47107
+ body: resolvedBody ?? "",
46376
47108
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
46377
47109
  relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
46378
47110
  validate: true
@@ -46387,12 +47119,14 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46387
47119
  "description",
46388
47120
  "system_prompt"
46389
47121
  ]);
47122
+ const promptBody = rulesyncSubagent.getBody();
47123
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
46390
47124
  const tomlObj = {
46391
47125
  agent_type: rawSection.agent_type === "agent" ? "agent" : "subagent",
46392
47126
  display_name: typeof rawSection.display_name === "string" ? rawSection.display_name : frontmatter.name,
46393
47127
  ...frontmatter.description ? { description: frontmatter.description } : {},
46394
- ...rulesyncSubagent.getBody() ? { system_prompt: rulesyncSubagent.getBody() } : {},
46395
- ...vibeSection
47128
+ ...vibeSection,
47129
+ ...promptBody ? { system_prompt_id: slug } : {}
46396
47130
  };
46397
47131
  const body = smolToml.stringify(tomlObj);
46398
47132
  const paths = this.getSettablePaths({ global });
@@ -46407,6 +47141,36 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46407
47141
  global
46408
47142
  });
46409
47143
  }
47144
+ /**
47145
+ * Emits the agent TOML plus its companion prompt file. The prompt file MUST
47146
+ * be written together with the id: `VibeConfigSchema._check_system_prompt`
47147
+ * evaluates the property during validation, so an unresolvable
47148
+ * `system_prompt_id` raises and `AgentRegistry._try_load` drops the agent
47149
+ * with a warning.
47150
+ */
47151
+ static fromRulesyncSubagents({ rulesyncSubagents, outputRoot = process.cwd(), global = false }) {
47152
+ return rulesyncSubagents.flatMap((rulesyncSubagent) => {
47153
+ const agentFile = VibeSubagent.fromRulesyncSubagent({
47154
+ outputRoot,
47155
+ relativeDirPath: VibeSubagent.getSettablePaths({ global }).relativeDirPath,
47156
+ rulesyncSubagent,
47157
+ global
47158
+ });
47159
+ const promptBody = rulesyncSubagent.getBody();
47160
+ if (!promptBody) return [agentFile];
47161
+ const slug = promptSlug(rulesyncSubagent.getRelativeFilePath());
47162
+ return [agentFile, new VibeSubagent({
47163
+ outputRoot,
47164
+ body: promptBody,
47165
+ relativeDirPath: VIBE_PROMPTS_DIR_PATH,
47166
+ relativeFilePath: `${slug}.md`,
47167
+ fileContent: promptBody,
47168
+ promptFile: true,
47169
+ validate: false,
47170
+ global
47171
+ })];
47172
+ });
47173
+ }
46410
47174
  validate() {
46411
47175
  try {
46412
47176
  VibeSubagentTomlSchema.parse(smolToml.parse(this.body));
@@ -46431,6 +47195,10 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46431
47195
  const paths = this.getSettablePaths({ global });
46432
47196
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
46433
47197
  const fileContent = await readFileContent(filePath);
47198
+ const resolvedSystemPrompt = await VibeSubagent.readSystemPrompt({
47199
+ outputRoot,
47200
+ fileContent
47201
+ });
46434
47202
  const subagent = new VibeSubagent({
46435
47203
  outputRoot,
46436
47204
  relativeDirPath: paths.relativeDirPath,
@@ -46438,7 +47206,8 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46438
47206
  body: fileContent.trim(),
46439
47207
  fileContent,
46440
47208
  validate,
46441
- global
47209
+ global,
47210
+ ...resolvedSystemPrompt !== void 0 && { resolvedSystemPrompt }
46442
47211
  });
46443
47212
  if (validate) {
46444
47213
  const result = subagent.validate();
@@ -46446,6 +47215,21 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
46446
47215
  }
46447
47216
  return subagent;
46448
47217
  }
47218
+ /**
47219
+ * Read `.vibe/prompts/<id>.md` for an agent that names a `system_prompt_id`.
47220
+ * Returns `undefined` when there is no id or the file is missing, in which
47221
+ * case import falls back to the legacy `system_prompt` key.
47222
+ */
47223
+ static async readSystemPrompt({ outputRoot, fileContent }) {
47224
+ let promptId;
47225
+ try {
47226
+ promptId = VibeSubagentTomlSchema.parse(smolToml.parse(fileContent)).system_prompt_id;
47227
+ } catch {
47228
+ return;
47229
+ }
47230
+ if (typeof promptId !== "string" || promptId === "" || /[\\/]/.test(promptId)) return;
47231
+ return await readFileContentOrNull(join(outputRoot, VIBE_PROMPTS_DIR_PATH, `${promptId}.md`)) ?? void 0;
47232
+ }
46449
47233
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46450
47234
  return new VibeSubagent({
46451
47235
  outputRoot,
@@ -46488,7 +47272,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46488
47272
  const mode = super.toRooMode(rulesyncSubagent);
46489
47273
  const frontmatter = rulesyncSubagent.getFrontmatter();
46490
47274
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
46491
- if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
47275
+ if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
46492
47276
  return mode;
46493
47277
  }
46494
47278
  toRulesyncSubagents() {
@@ -46500,7 +47284,7 @@ var ZoocodeSubagent = class extends RooSubagent {
46500
47284
  targets: ["zoocode"],
46501
47285
  roo: restRooSection
46502
47286
  };
46503
- if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
47287
+ if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
46504
47288
  return new RulesyncSubagent({
46505
47289
  outputRoot: ".",
46506
47290
  frontmatter: rebuilt,
@@ -49403,8 +50187,13 @@ const STRATEGIES = [
49403
50187
  * file per rule under `.devin/rules/*.md` (Devin Desktop Cascade) with YAML
49404
50188
  * frontmatter carrying a `trigger` (always_on | glob | manual | model_decision)
49405
50189
  * plus companion `globs`/`description` fields.
49406
- * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
49407
- * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
50190
+ * - Global scope: mirrors the project layout. The root rule is a single
50191
+ * plain-markdown, always-on file (no frontmatter) at
50192
+ * `~/.config/devin/AGENTS.md`; non-root rules are one file per rule under
50193
+ * `~/.devin/rules/*.md` with the same `trigger`/`globs` frontmatter as project
50194
+ * scope. Note the directory split — the per-rule global directory is the home
50195
+ * `~/.devin/`, not the `~/.config/devin/` tree every other Devin global
50196
+ * surface uses. That is what the rules page documents.
49408
50197
  *
49409
50198
  * Trigger inference (when no explicit devin trigger is persisted):
49410
50199
  * - Specific globs (non wildcard) → glob
@@ -49435,19 +50224,32 @@ var DevinRule = class DevinRule extends ToolRule {
49435
50224
  relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
49436
50225
  };
49437
50226
  }
50227
+ /**
50228
+ * The always-on root rule file for a scope: the project-root `AGENTS.md`
50229
+ * Devin CLI / Devin Local reads, or `~/.config/devin/AGENTS.md` in global
50230
+ * scope. Single-sourced so `getSettablePaths`, `fromFile` and
50231
+ * `fromRulesyncRule` cannot disagree about it.
50232
+ */
50233
+ static getRootPath(global, excludeToolDir) {
50234
+ return global ? DevinRule.getGlobalRootPath(excludeToolDir) : {
50235
+ relativeDirPath: ".",
50236
+ relativeFilePath: "AGENTS.md"
50237
+ };
50238
+ }
49438
50239
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
49439
- if (global) return { root: DevinRule.getGlobalRootPath(excludeToolDir) };
50240
+ if (global) return {
50241
+ root: DevinRule.getRootPath(true, excludeToolDir),
50242
+ nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
50243
+ };
49440
50244
  return {
49441
- root: {
49442
- relativeDirPath: ".",
49443
- relativeFilePath: "AGENTS.md"
49444
- },
50245
+ root: DevinRule.getRootPath(false, excludeToolDir),
49445
50246
  nonRoot: { relativeDirPath: buildToolPath(DEVIN_DIR, "rules", excludeToolDir) }
49446
50247
  };
49447
50248
  }
49448
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
49449
- if (global) {
49450
- const rootPath = DevinRule.getGlobalRootPath();
50249
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
50250
+ const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
50251
+ if (relativeDirPath !== void 0 ? relativeDirPath !== nonRootDirPath : relativeFilePath === DevinRule.getRootPath(global).relativeFilePath) {
50252
+ const rootPath = DevinRule.getRootPath(global);
49451
50253
  const fileContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
49452
50254
  return new DevinRule({
49453
50255
  outputRoot,
@@ -49459,19 +50261,6 @@ var DevinRule = class DevinRule extends ToolRule {
49459
50261
  root: true
49460
50262
  });
49461
50263
  }
49462
- if (relativeFilePath === "AGENTS.md") {
49463
- const fileContent = await readFileContent(join(outputRoot, relativeFilePath));
49464
- return new DevinRule({
49465
- outputRoot,
49466
- relativeDirPath: ".",
49467
- relativeFilePath: "AGENTS.md",
49468
- frontmatter: {},
49469
- body: fileContent,
49470
- validate,
49471
- root: true
49472
- });
49473
- }
49474
- const nonRootDirPath = buildToolPath(DEVIN_DIR, "rules");
49475
50264
  const filePath = join(outputRoot, nonRootDirPath, relativeFilePath);
49476
50265
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
49477
50266
  let parsedFrontmatter;
@@ -49491,8 +50280,9 @@ var DevinRule = class DevinRule extends ToolRule {
49491
50280
  });
49492
50281
  }
49493
50282
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
49494
- if (global) {
49495
- const rootPath = DevinRule.getGlobalRootPath();
50283
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
50284
+ if (rulesyncFrontmatter.root) {
50285
+ const rootPath = DevinRule.getRootPath(global);
49496
50286
  return new DevinRule({
49497
50287
  outputRoot,
49498
50288
  relativeDirPath: rootPath.relativeDirPath,
@@ -49503,16 +50293,6 @@ var DevinRule = class DevinRule extends ToolRule {
49503
50293
  root: true
49504
50294
  });
49505
50295
  }
49506
- const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
49507
- if (rulesyncFrontmatter.root) return new DevinRule({
49508
- outputRoot,
49509
- relativeDirPath: ".",
49510
- relativeFilePath: "AGENTS.md",
49511
- frontmatter: {},
49512
- body: rulesyncRule.getBody(),
49513
- validate,
49514
- root: true
49515
- });
49516
50296
  const storedDevin = rulesyncFrontmatter.devin;
49517
50297
  const normalized = normalizeStoredDevin(storedDevin);
49518
50298
  const storedTrigger = storedDevin?.trigger;
@@ -49572,7 +50352,7 @@ var DevinRule = class DevinRule extends ToolRule {
49572
50352
  error: null
49573
50353
  };
49574
50354
  }
49575
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50355
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
49576
50356
  return new DevinRule({
49577
50357
  outputRoot,
49578
50358
  relativeDirPath,
@@ -49580,7 +50360,7 @@ var DevinRule = class DevinRule extends ToolRule {
49580
50360
  frontmatter: {},
49581
50361
  body: "",
49582
50362
  validate: false,
49583
- root: global
50363
+ root: relativeDirPath !== buildToolPath(DEVIN_DIR, "rules")
49584
50364
  });
49585
50365
  }
49586
50366
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -50213,6 +50993,11 @@ var KimiCodeRule = class KimiCodeRule extends ToolRule {
50213
50993
  };
50214
50994
  //#endregion
50215
50995
  //#region src/features/rules/kiro-rule.ts
50996
+ /**
50997
+ * Kiro loads `AGENTS.md` as steering context from any directory in the
50998
+ * workspace tree (CLI 2.18.0 / IDE 1.0.309), not just the workspace root.
50999
+ */
51000
+ const KIRO_NESTED_STEERING_FILE_NAME = "AGENTS.md";
50216
51001
  const WILDCARD_GLOBS = /* @__PURE__ */ new Set([
50217
51002
  "**/*",
50218
51003
  "**",
@@ -50286,8 +51071,17 @@ var KiroRule = class KiroRule extends ToolRule {
50286
51071
  };
50287
51072
  return { nonRoot: { relativeDirPath: steeringDirPath } };
50288
51073
  }
50289
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
51074
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
50290
51075
  const paths = this.getSettablePaths({ global });
51076
+ const steeringDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
51077
+ if (overrideDirPath !== void 0 && overrideDirPath !== steeringDirPath && overrideDirPath !== "." && relativeFilePath === KIRO_NESTED_STEERING_FILE_NAME) return new KiroRule({
51078
+ outputRoot,
51079
+ relativeDirPath: overrideDirPath,
51080
+ relativeFilePath,
51081
+ fileContent: await readFileContent(join(outputRoot, overrideDirPath, relativeFilePath)),
51082
+ validate,
51083
+ root: false
51084
+ });
50291
51085
  const isRoot = "root" in paths && relativeFilePath === paths.root.relativeFilePath;
50292
51086
  const relativeDirPath = paths.nonRoot?.relativeDirPath ?? buildToolPath(".kiro", "steering");
50293
51087
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
@@ -50311,6 +51105,12 @@ var KiroRule = class KiroRule extends ToolRule {
50311
51105
  });
50312
51106
  if (params.root) return new KiroRule(params);
50313
51107
  const frontmatter = rulesyncRule.getFrontmatter();
51108
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
51109
+ if (!global && subprojectPath) return new KiroRule({
51110
+ ...params,
51111
+ relativeDirPath: join(subprojectPath),
51112
+ relativeFilePath: KIRO_NESTED_STEERING_FILE_NAME
51113
+ });
50314
51114
  const inclusion = deriveKiroInclusion({
50315
51115
  kiro: frontmatter.kiro,
50316
51116
  globs: frontmatter.globs
@@ -50321,7 +51121,34 @@ var KiroRule = class KiroRule extends ToolRule {
50321
51121
  fileContent: stringifyFrontmatter(rulesyncRule.getBody(), inclusion)
50322
51122
  });
50323
51123
  }
51124
+ /**
51125
+ * The subproject directory a nested `AGENTS.md` scopes, or `undefined` for the
51126
+ * root rule and for the `.kiro/steering/` files.
51127
+ */
51128
+ getSubprojectPath() {
51129
+ if (this.isRoot() || this.getRelativeFilePath() !== KIRO_NESTED_STEERING_FILE_NAME) return;
51130
+ const relativeDirPath = toPosixPath(this.getRelativeDirPath());
51131
+ if (relativeDirPath === "." || relativeDirPath === "" || relativeDirPath.startsWith(".")) return;
51132
+ return relativeDirPath;
51133
+ }
50324
51134
  toRulesyncRule() {
51135
+ const subprojectPath = this.getSubprojectPath();
51136
+ if (subprojectPath !== void 0) {
51137
+ const slug = subprojectPath.replaceAll("/", "-");
51138
+ return new RulesyncRule({
51139
+ outputRoot: this.getOutputRoot(),
51140
+ relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
51141
+ relativeFilePath: `${slug}-kiro.md`,
51142
+ frontmatter: {
51143
+ targets: ["kiro"],
51144
+ root: false,
51145
+ globs: [`${subprojectPath}/**/*`],
51146
+ agentsmd: { subprojectPath }
51147
+ },
51148
+ body: this.getFileContent(),
51149
+ validate: true
51150
+ });
51151
+ }
50325
51152
  const { frontmatter, body } = parseFrontmatter(this.getFileContent(), this.getRelativeFilePath());
50326
51153
  const inclusion = typeof frontmatter.inclusion === "string" ? frontmatter.inclusion : void 0;
50327
51154
  if (!inclusion) return this.toRulesyncRuleDefault();
@@ -50354,6 +51181,26 @@ var KiroRule = class KiroRule extends ToolRule {
50354
51181
  error: null
50355
51182
  };
50356
51183
  }
51184
+ /**
51185
+ * Nested `AGENTS.md` files anywhere in the workspace tree, which Kiro loads as
51186
+ * steering context alongside `.kiro/steering/`. Import-only, like every other
51187
+ * nested scan: these are hand-authored files outside a rulesync-owned
51188
+ * directory, so enumerating them for `--delete` would sweep away work rulesync
51189
+ * never wrote.
51190
+ * @see https://kiro.dev/docs/steering/
51191
+ */
51192
+ static getNestedFilePatterns({ outputRoot }) {
51193
+ const root = toPosixPath(outputRoot);
51194
+ return {
51195
+ include: [`${root}/**/${KIRO_NESTED_STEERING_FILE_NAME}`],
51196
+ ignore: [
51197
+ `${root}/${KIRO_NESTED_STEERING_FILE_NAME}`,
51198
+ `${root}/**/.*/**`,
51199
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
51200
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
51201
+ ]
51202
+ };
51203
+ }
50357
51204
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
50358
51205
  return new KiroRule({
50359
51206
  outputRoot,
@@ -51822,6 +52669,18 @@ const resolveIncludeLocalRoot = (options) => {
51822
52669
  return parsed.data.includeLocalRoot ?? true;
51823
52670
  };
51824
52671
  /**
52672
+ * Whether `candidate` is `base` itself or a class that extends it.
52673
+ *
52674
+ * Class-identity dispatch (`factory.class === Base`) is subclass-blind: a target
52675
+ * whose adapter merely narrows another one — `ZoocodeRule extends RooRule` — fails
52676
+ * a strict `===` and silently falls through. That is how `zoocode` ended up
52677
+ * declaring `localRootMode: "separate-local-file"` while never emitting
52678
+ * `AGENTS.local.md`, in both directions and without a warning.
52679
+ */
52680
+ function isClassOrSubclassOf({ candidate, base }) {
52681
+ return candidate === base || Object.prototype.isPrototypeOf.call(base, candidate);
52682
+ }
52683
+ /**
51825
52684
  * Factory Map mapping tool targets to their rule factories.
51826
52685
  * Using Map to preserve insertion order for consistent iteration.
51827
52686
  */
@@ -52175,7 +53034,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
52175
53034
  meta: {
52176
53035
  extension: "md",
52177
53036
  supportsGlobal: true,
52178
- ruleDiscoveryMode: "auto"
53037
+ ruleDiscoveryMode: "auto",
53038
+ localRootMode: "separate-local-file",
53039
+ localRootFileName: "AGENTS.local.md"
52179
53040
  }
52180
53041
  }],
52181
53042
  ["zed", {
@@ -52462,7 +53323,10 @@ var RulesProcessor = class extends FeatureProcessor {
52462
53323
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
52463
53324
  }
52464
53325
  buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
52465
- if (factory.class === ClaudecodeRule) {
53326
+ if (isClassOrSubclassOf({
53327
+ candidate: factory.class,
53328
+ base: ClaudecodeRule
53329
+ })) {
52466
53330
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
52467
53331
  return new ClaudecodeRule({
52468
53332
  outputRoot: this.outputRoot,
@@ -52475,7 +53339,10 @@ var RulesProcessor = class extends FeatureProcessor {
52475
53339
  localRoot
52476
53340
  });
52477
53341
  }
52478
- if (factory.class === ClaudecodeLegacyRule) {
53342
+ if (isClassOrSubclassOf({
53343
+ candidate: factory.class,
53344
+ base: ClaudecodeLegacyRule
53345
+ })) {
52479
53346
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
52480
53347
  return new ClaudecodeLegacyRule({
52481
53348
  outputRoot: this.outputRoot,
@@ -52487,7 +53354,10 @@ var RulesProcessor = class extends FeatureProcessor {
52487
53354
  localRoot
52488
53355
  });
52489
53356
  }
52490
- if (factory.class === RovodevRule) return new RovodevRule({
53357
+ if (isClassOrSubclassOf({
53358
+ candidate: factory.class,
53359
+ base: RovodevRule
53360
+ })) return new RovodevRule({
52491
53361
  outputRoot: this.outputRoot,
52492
53362
  relativeDirPath: relativeDirPath ?? ".",
52493
53363
  relativeFilePath: fileName,
@@ -52496,7 +53366,10 @@ var RulesProcessor = class extends FeatureProcessor {
52496
53366
  root: true,
52497
53367
  localRoot
52498
53368
  });
52499
- if (factory.class === RooRule) return new RooRule({
53369
+ if (isClassOrSubclassOf({
53370
+ candidate: factory.class,
53371
+ base: RooRule
53372
+ })) return new RooRule({
52500
53373
  outputRoot: this.outputRoot,
52501
53374
  relativeDirPath: relativeDirPath ?? ".",
52502
53375
  relativeFilePath: fileName,
@@ -52505,7 +53378,23 @@ var RulesProcessor = class extends FeatureProcessor {
52505
53378
  root: true,
52506
53379
  localRoot
52507
53380
  });
52508
- if (factory.class === QwencodeRule) return new QwencodeRule({
53381
+ if (isClassOrSubclassOf({
53382
+ candidate: factory.class,
53383
+ base: DevinRule
53384
+ })) return new DevinRule({
53385
+ outputRoot: this.outputRoot,
53386
+ relativeDirPath: relativeDirPath ?? ".",
53387
+ relativeFilePath: fileName,
53388
+ frontmatter: {},
53389
+ body,
53390
+ validate: true,
53391
+ root: true,
53392
+ localRoot
53393
+ });
53394
+ if (isClassOrSubclassOf({
53395
+ candidate: factory.class,
53396
+ base: QwencodeRule
53397
+ })) return new QwencodeRule({
52509
53398
  outputRoot: this.outputRoot,
52510
53399
  relativeDirPath: relativeDirPath ?? ".qwen",
52511
53400
  relativeFilePath: fileName,
@@ -54722,4 +55611,4 @@ async function importChecksCore(params) {
54722
55611
  //#endregion
54723
55612
  export { JsonLogger as $, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, ALL_TOOL_TARGETS_WITH_WILDCARD as At, RulesyncCheck as B, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileBuffer as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_CHECKS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_LEGACY_FILE_NAME as Jt, ConfigResolver as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kt, parseJsonc as L, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Lt, RulesyncMcp as M, ToolTargetSchema as Mt, RulesyncIgnore as N, MAX_FILE_SIZE as Nt, RulesyncSkillFrontmatterSchema as O, writeFileContent as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_FILE_NAME as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_MCP_SCHEMA_URL as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_RELATIVE_FILE_PATH as Yt, findControlCharacter as Z, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, ALL_FEATURES_WITH_WILDCARD as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SKILLS_RELATIVE_DIR_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, PACKAGING_TOOL_TARGETS as jt, RulesyncRule as k, ALL_TOOL_TARGETS as kt, SkillsProcessor as l, DEPRECATED_FEATURE_REPLACEMENTS as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_FILE_NAME as qt, generate as r, RULESYNC_RULES_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_PERMISSIONS_SCHEMA_URL as tn, warnOnConflictingFlags as tt, McpProcessor as u, formatError as un, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CONFIG_SCHEMA_URL as zt };
54724
55613
 
54725
- //# sourceMappingURL=import-Bgkf_jVN.js.map
55614
+ //# sourceMappingURL=import-Dswkc7Ub.js.map