rulesync 26.0.0 → 27.0.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.
@@ -687,6 +687,7 @@ const subagentsProcessorToolTargetTuple = [
687
687
  "antigravity-plugin",
688
688
  "augmentcode",
689
689
  "augmentcode-plugin",
690
+ "bob",
690
691
  "claudecode",
691
692
  "claudecode-plugin",
692
693
  "claudecode-legacy",
@@ -853,6 +854,7 @@ const permissionsProcessorToolTargetTuple = [
853
854
  "claudecode",
854
855
  "cline",
855
856
  "codebuddy",
857
+ "codewhale",
856
858
  "codexcli",
857
859
  "commandcode",
858
860
  "continue",
@@ -874,6 +876,7 @@ const permissionsProcessorToolTargetTuple = [
874
876
  "kiro-ide",
875
877
  "lettacode",
876
878
  "mimocode",
879
+ "omp",
877
880
  "opencode",
878
881
  "pi",
879
882
  "pool",
@@ -897,6 +900,7 @@ const checksProcessorToolTargetTuple = [
897
900
  "factorydroid",
898
901
  "gitlabduo",
899
902
  "hermesagent",
903
+ "kilo",
900
904
  "qwencode",
901
905
  "rovodev",
902
906
  "takt"
@@ -3691,6 +3695,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3691
3695
  ".grok/sandbox.toml",
3692
3696
  ".letta/settings.json",
3693
3697
  ".qoder/settings.json",
3698
+ ".omp/config.yml",
3699
+ ".omp/agent/config.yml",
3694
3700
  ".rovodev/config.yml",
3695
3701
  ".rovodev/mcp.json",
3696
3702
  ".poolside/settings.yaml",
@@ -7664,6 +7670,32 @@ const ToolScopedPermissionSchema = z.record(PermissionCategorySchema, Permission
7664
7670
  * added without a schema break.
7665
7671
  */
7666
7672
  const CanonicalPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
7673
+ /**
7674
+ * One Codewhale `[[rules]]` record of `~/.codewhale/permissions.toml`, written
7675
+ * verbatim. Codewhale parses the file with unknown fields denied, so a single
7676
+ * misspelled key would make it reject every rule in the file; the record is
7677
+ * therefore strict here, surfacing the typo on the rulesync source instead.
7678
+ *
7679
+ * @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
7680
+ */
7681
+ const CodewhalePermissionRuleSchema = z.strictObject({
7682
+ tool: z.string(),
7683
+ command: z.optional(z.string()),
7684
+ command_exact: z.optional(z.boolean()),
7685
+ path: z.optional(z.string()),
7686
+ workspace: z.optional(z.string()),
7687
+ action: z.optional(PermissionActionSchema)
7688
+ });
7689
+ /**
7690
+ * Tool-scoped override block for Codewhale. `rules` carries native records the
7691
+ * canonical category/pattern shape cannot express — a `workspace`-scoped rule,
7692
+ * a tool rulesync does not map, an exact-command deny — and is emitted ahead of
7693
+ * the rules converted from the canonical block.
7694
+ */
7695
+ const CodewhalePermissionsOverrideSchema = z.looseObject({
7696
+ permission: z.optional(ToolScopedPermissionSchema),
7697
+ rules: z.optional(z.array(CodewhalePermissionRuleSchema))
7698
+ });
7667
7699
  const KimiCodePermissionsOverrideSchema = z.looseObject({
7668
7700
  permission: z.optional(ToolScopedPermissionSchema),
7669
7701
  defaultPermissionMode: z.optional(z.enum([
@@ -7884,12 +7916,15 @@ const CursorPermissionsOverrideSchema = z.looseObject({
7884
7916
  * autonomy/sandbox controls with no canonical permission category — under
7885
7917
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
7886
7918
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
7887
- * `todoWrite`, `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
7888
- * `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
7889
- * `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
7919
+ * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
7920
+ * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
7921
+ * `allowedInsecureVoiceBaseUrls`). Qwen Code strips `tools.workflowsEnabled`,
7922
+ * `tools.executionSandbox`, `security.allowPrivateNetworkHooks` and
7890
7923
  * `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
7891
- * skips those three in project scope and announces a granting value in global
7892
- * scope. `security.allowedHttpHookUrls` (honored in a workspace only while no
7924
+ * skips those four in project scope and announces a granting value in global
7925
+ * scope (any `executionSandbox` change counts). `tools.workflowNameOnly` is
7926
+ * written in both scopes, but a workspace may only turn it on, which the
7927
+ * project-scope note says. `security.allowedHttpHookUrls` (honored in a workspace only while no
7893
7928
  * higher scope sets it) and `security.folderTrust` (the initial trust decision
7894
7929
  * is made from user/system settings alone, before the workspace merge) are
7895
7930
  * written in both scopes, with a note in project scope and an announcement of
@@ -8777,6 +8812,7 @@ const PermissionsConfigSchema = z.looseObject({
8777
8812
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
8778
8813
  kiro: z.optional(KiroPermissionsOverrideSchema),
8779
8814
  codexcli: z.optional(CodexcliPermissionsOverrideSchema),
8815
+ codewhale: z.optional(CodewhalePermissionsOverrideSchema),
8780
8816
  zed: z.optional(ZedPermissionsOverrideSchema),
8781
8817
  devin: z.optional(DevinPermissionsOverrideSchema),
8782
8818
  "antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
@@ -8789,6 +8825,7 @@ const PermissionsConfigSchema = z.looseObject({
8789
8825
  copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
8790
8826
  crush: z.optional(CanonicalPermissionsOverrideSchema),
8791
8827
  goose: z.optional(CanonicalPermissionsOverrideSchema),
8828
+ omp: z.optional(CanonicalPermissionsOverrideSchema),
8792
8829
  pool: z.optional(CanonicalPermissionsOverrideSchema),
8793
8830
  qoder: z.optional(CanonicalPermissionsOverrideSchema),
8794
8831
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
@@ -10722,6 +10759,13 @@ const RulesyncSubagentFrontmatterSchema = z.looseObject({
10722
10759
  customInstructions: z.optional(z.string()),
10723
10760
  groups: z.optional(z.array(z.unknown()))
10724
10761
  })),
10762
+ bob: z.optional(z.looseObject({
10763
+ slug: z.optional(z.string()),
10764
+ whenToUse: z.optional(z.string()),
10765
+ roleDefinition: z.optional(z.string()),
10766
+ customInstructions: z.optional(z.string()),
10767
+ groups: z.optional(z.array(z.unknown()))
10768
+ })),
10725
10769
  zoocode: z.optional(z.looseObject({
10726
10770
  /** Per-mode MCP server allowlist (Zoo Code v3.60.0+); omitted = all. */
10727
10771
  allowedMcpServers: z.optional(z.array(z.string())) })),
@@ -11062,6 +11106,27 @@ const GITLABDUO_SKILLS_DIR = "skills";
11062
11106
  /** User skills (requires `--enable-global-skills` / `GITLAB_ENABLE_GLOBAL_SKILLS`). */
11063
11107
  const GITLABDUO_GLOBAL_SKILLS_DIR = join(GITLABDUO_DIR, "skills");
11064
11108
  //#endregion
11109
+ //#region src/constants/kilo-paths.ts
11110
+ const KILO_DIR = ".kilo";
11111
+ const KILO_GLOBAL_DIR = join(".config", "kilo");
11112
+ const KILO_RULES_DIR_NAME = "rules";
11113
+ const KILO_COMMANDS_DIR_PATH = join(KILO_DIR, "commands");
11114
+ const KILO_GLOBAL_COMMANDS_DIR_PATH = join(KILO_GLOBAL_DIR, "commands");
11115
+ const KILO_SKILLS_DIR_PATH = join(KILO_DIR, "skills");
11116
+ const KILO_AGENTS_DIR_PATH = join(KILO_DIR, "agents");
11117
+ const KILO_GLOBAL_AGENTS_DIR_PATH = join(KILO_GLOBAL_DIR, "agents");
11118
+ const KILO_PLUGINS_DIR_PATH = join(KILO_DIR, "plugins");
11119
+ const KILO_GLOBAL_PLUGINS_DIR_PATH = join(KILO_GLOBAL_DIR, "plugins");
11120
+ const KILO_PLUGIN_DIR_PATH = join(KILO_DIR, "plugin");
11121
+ const KILO_GLOBAL_PLUGIN_DIR_PATH = join(KILO_GLOBAL_DIR, "plugin");
11122
+ const KILO_RULE_FILE_NAME = "AGENTS.md";
11123
+ const KILO_IGNORE_FILE_NAME = ".kilocodeignore";
11124
+ const KILO_LEGACY_GLOBAL_DIR = ".kilocode";
11125
+ const KILO_JSONC_FILE_NAME = "kilo.jsonc";
11126
+ const KILO_JSON_FILE_NAME = "kilo.json";
11127
+ const KILO_HOOKS_FILE_NAME = "rulesync-hooks.js";
11128
+ const KILO_REVIEW_FILE_NAME = "REVIEW.md";
11129
+ //#endregion
11065
11130
  //#region src/constants/qwencode-paths.ts
11066
11131
  const QWENCODE_DIR = ".qwen";
11067
11132
  const QWENCODE_COMMANDS_DIR_PATH = join(QWENCODE_DIR, "commands");
@@ -11453,8 +11518,14 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
11453
11518
  async removeOrphanAiFiles(existingFiles, generatedFiles) {
11454
11519
  const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
11455
11520
  const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
11521
+ let removedCount = 0;
11456
11522
  for (const aiFile of orphanFiles) {
11457
11523
  const filePath = aiFile.getFilePath();
11524
+ if (await refusesDeleteOutsideRoot({
11525
+ logger: this.logger,
11526
+ rootPath: aiFile.getOutputRoot(),
11527
+ filePath
11528
+ })) continue;
11458
11529
  const loggedPath = stripControlCharacters(filePath);
11459
11530
  if (this.dryRun) this.logger.info(`[DRY RUN] Would delete: ${loggedPath}`);
11460
11531
  else {
@@ -11465,8 +11536,9 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
11465
11536
  path: aiFile.getRelativePathFromCwd(),
11466
11537
  kind: "file"
11467
11538
  });
11539
+ removedCount++;
11468
11540
  }
11469
- return orphanFiles.length;
11541
+ return removedCount;
11470
11542
  }
11471
11543
  /**
11472
11544
  * The paths {@link removeOrphanAiFiles} deleted, or would have deleted under
@@ -11574,6 +11646,25 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
11574
11646
  return true;
11575
11647
  }
11576
11648
  /**
11649
+ * Whether deleting `filePath` has to be refused because a link on the way to
11650
+ * the directory holding it leads out of `rootPath`, warning about the refusal.
11651
+ *
11652
+ * Deletion candidates are enumerated without following links *inside* the
11653
+ * scanned directory, but the scanned directory itself is reached through
11654
+ * whatever links its path contains: a checked-out repository can carry
11655
+ * `.cursor/commands -> ~/notes`, and every file in `~/notes` would then read as
11656
+ * an orphan. Only the parent is judged, because removing a file that is itself
11657
+ * a link unlinks the link where it is spelled and never touches its target.
11658
+ */
11659
+ async function refusesDeleteOutsideRoot({ logger, rootPath, filePath }) {
11660
+ if (!await writablePathEscapesRoot({
11661
+ rootPath,
11662
+ targetPath: dirname(filePath)
11663
+ })) return false;
11664
+ logger.warn(`Refusing to delete ${quoteForLog(filePath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
11665
+ return true;
11666
+ }
11667
+ /**
11577
11668
  * {@link refusesWriteOutsideRoot} over every path a single unit of output
11578
11669
  * writes, stopping at the first refusal: a directory written as one unit is
11579
11670
  * held back whole when any one file in it would land outside the root.
@@ -12716,7 +12807,7 @@ function stripSkillFrontmatter(fileContent) {
12716
12807
  content = rest.slice(closing.index + closing[0].length).replace(LEADING_BLANK_LINE_PATTERN, "");
12717
12808
  }
12718
12809
  }
12719
- function handWrittenWarning({ filePath, displayName, toolTarget }) {
12810
+ function handWrittenWarning$1({ filePath, displayName, toolTarget }) {
12720
12811
  return `${displayName} checks: ${filePath} holds instructions rulesync did not write, so it is left as it is and no checks were generated for ${displayName}. Run \`rulesync import --targets ${toolTarget} --features checks\` to bring them into \`.rulesync/checks/\` and then delete the file, so the next generate writes it back from there; delete it outright if you no longer want it, or rename the directory if it is an ordinary skill rather than review guidelines. Importing alone leaves this file as it is, so it keeps blocking generation until it is gone. A rulesync skill named \`${FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME}\` is no longer generated here — Factory's reviewer reads this path, so the checks feature owns it — but a directory an older rulesync wrote can still be sitting there.`;
12721
12812
  }
12722
12813
  /**
@@ -12773,7 +12864,7 @@ var FactorydroidCheck = class extends AggregatedToolCheck {
12773
12864
  toolTarget: "factorydroid",
12774
12865
  fallbackCheckName: FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME,
12775
12866
  handWrittenPreamble: "skip",
12776
- handWrittenWarning,
12867
+ handWrittenWarning: handWrittenWarning$1,
12777
12868
  transformImportedContent: stripSkillFrontmatter
12778
12869
  };
12779
12870
  }
@@ -13254,6 +13345,51 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
13254
13345
  }
13255
13346
  };
13256
13347
  //#endregion
13348
+ //#region src/features/checks/kilo-check.ts
13349
+ function handWrittenWarning({ filePath, displayName, toolTarget }) {
13350
+ return `${displayName} checks: ${filePath} holds instructions rulesync did not write, so it is left as it is and no checks were generated for ${displayName}. Run \`rulesync import --targets ${toolTarget} --features checks\` to bring them into \`.rulesync/checks/\` and then delete the file, so the next generate writes it back from there. Importing alone leaves this file as it is, so it keeps blocking generation until it is gone.`;
13351
+ }
13352
+ /**
13353
+ * Checks adapter for Kilo Code Reviews' repository guidance (root `REVIEW.md`).
13354
+ *
13355
+ * The Kilo review agent reads one plain-Markdown file at the repository root
13356
+ * (from the PR/MR base branch, once "Use REVIEW.md" is enabled in the Kilo web
13357
+ * app), so every `.rulesync/checks/*.md` targeting Kilo collapses into it via
13358
+ * the `fromRulesyncChecks` on {@link AggregatedToolCheck}, with each check
13359
+ * written as a marked section (see `aggregated-check-file.ts` for the marker
13360
+ * convention).
13361
+ *
13362
+ * The reviewer reads the file as free prose, so a check's `severity` and
13363
+ * `tools` have no equivalent there: they are not written and do not come back
13364
+ * on import. Neither does `description` whenever the check also has a body.
13365
+ *
13366
+ * Project scope only — the reviewer reads the file from the repository and
13367
+ * there is no user-level equivalent.
13368
+ *
13369
+ * A root-level `REVIEW.md` is a generic name other reviewers may read too, so
13370
+ * unlike Cursor Bugbot's tool-specific `.cursor/BUGBOT.md` a hand-written file
13371
+ * here is left untouched (the `skip` policy) rather than replaced.
13372
+ *
13373
+ * @see https://kilo.ai/docs/automate/code-reviews
13374
+ */
13375
+ var KiloCheck = class extends AggregatedToolCheck {
13376
+ static getSettablePaths(_options = {}) {
13377
+ return {
13378
+ relativeDirPath: ".",
13379
+ relativeFilePath: KILO_REVIEW_FILE_NAME
13380
+ };
13381
+ }
13382
+ static getAggregatedCheckConfig() {
13383
+ return {
13384
+ displayName: "Kilo Code",
13385
+ toolTarget: "kilo",
13386
+ fallbackCheckName: "review",
13387
+ handWrittenPreamble: "skip",
13388
+ handWrittenWarning
13389
+ };
13390
+ }
13391
+ };
13392
+ //#endregion
13257
13393
  //#region src/features/checks/qwencode-check.ts
13258
13394
  /**
13259
13395
  * Checks adapter for Qwen Code's `/review` project rules (`.qwen/review-rules.md`).
@@ -13350,6 +13486,12 @@ const CLAUDECODE_LOCAL_RULE_FILE_NAME = "CLAUDE.local.md";
13350
13486
  const CLAUDECODE_RULES_DIR_NAME = "rules";
13351
13487
  /** Memories directory name under `.claude/` (legacy format). */
13352
13488
  const CLAUDECODE_MEMORIES_DIR_NAME = "memories";
13489
+ /**
13490
+ * Directory under `.claude/` where Claude Code keeps the memory of a subagent
13491
+ * declared with `memory: local`; documented as not checked into version control.
13492
+ * @see https://code.claude.com/docs/en/sub-agents#enable-persistent-memory
13493
+ */
13494
+ const CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME = "agent-memory-local";
13353
13495
  const CLAUDECODE_COMMANDS_DIR_PATH = join(CLAUDECODE_DIR, "commands");
13354
13496
  const CLAUDECODE_AGENTS_DIR_PATH = join(CLAUDECODE_DIR, "agents");
13355
13497
  const CLAUDECODE_SKILLS_DIR_PATH = join(CLAUDECODE_DIR, "skills");
@@ -14530,6 +14672,22 @@ const SHARED_CONFIG_OWNERSHIP = {
14530
14672
  }
14531
14673
  }
14532
14674
  },
14675
+ ".omp/config.yml": {
14676
+ format: "yaml",
14677
+ invalidRootPolicy: "error",
14678
+ features: { permissions: {
14679
+ kind: "replace-owned-keys",
14680
+ ownedKeys: ["tools", "bash"]
14681
+ } }
14682
+ },
14683
+ ".omp/agent/config.yml": {
14684
+ format: "yaml",
14685
+ invalidRootPolicy: "error",
14686
+ features: { permissions: {
14687
+ kind: "replace-owned-keys",
14688
+ ownedKeys: ["tools", "bash"]
14689
+ } }
14690
+ },
14533
14691
  ".pi/settings.json": {
14534
14692
  format: "json",
14535
14693
  invalidRootPolicy: "error",
@@ -15576,6 +15734,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
15576
15734
  filePattern: "*.json"
15577
15735
  }
15578
15736
  }],
15737
+ ["kilo", {
15738
+ class: KiloCheck,
15739
+ meta: {
15740
+ supportsGlobal: false,
15741
+ filePattern: KILO_REVIEW_FILE_NAME,
15742
+ committedOutput: true
15743
+ }
15744
+ }],
15579
15745
  ["qwencode", {
15580
15746
  class: QwencodeCheck,
15581
15747
  meta: {
@@ -16357,6 +16523,7 @@ const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
16357
16523
  const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
16358
16524
  const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
16359
16525
  const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
16526
+ const ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR = join(ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "rules");
16360
16527
  const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
16361
16528
  const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
16362
16529
  const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
@@ -16635,6 +16802,7 @@ const BOB_COMMANDS_DIR_PATH = join(BOB_DIR, "commands");
16635
16802
  const BOB_SKILLS_DIR_PATH = join(BOB_DIR, "skills");
16636
16803
  const BOB_SETTINGS_FILE_NAME = "settings.json";
16637
16804
  const BOB_GLOBAL_SETTINGS_DIR_PATH = join(BOB_DIR, "settings");
16805
+ const BOB_CUSTOM_MODES_FILE_NAME = "custom_modes.yaml";
16638
16806
  //#endregion
16639
16807
  //#region src/features/commands/bob-command.ts
16640
16808
  /**
@@ -17164,6 +17332,7 @@ const CODEWHALE_COMMANDS_DIR_PATH = join(CODEWHALE_DIR, "commands");
17164
17332
  const CODEWHALE_HOOKS_FILE_NAME = "hooks.toml";
17165
17333
  const CODEWHALE_CONFIG_FILE_NAME = "config.toml";
17166
17334
  const CODEWHALE_MCP_FILE_NAME = "mcp.json";
17335
+ const CODEWHALE_PERMISSIONS_FILE_NAME = "permissions.toml";
17167
17336
  //#endregion
17168
17337
  //#region src/features/commands/codewhale-command.ts
17169
17338
  /**
@@ -20096,25 +20265,6 @@ var JunieCommand = class JunieCommand extends ToolCommand {
20096
20265
  }
20097
20266
  };
20098
20267
  //#endregion
20099
- //#region src/constants/kilo-paths.ts
20100
- const KILO_DIR = ".kilo";
20101
- const KILO_GLOBAL_DIR = join(".config", "kilo");
20102
- const KILO_RULES_DIR_NAME = "rules";
20103
- const KILO_COMMANDS_DIR_PATH = join(KILO_DIR, "commands");
20104
- const KILO_GLOBAL_COMMANDS_DIR_PATH = join(KILO_GLOBAL_DIR, "commands");
20105
- const KILO_SKILLS_DIR_PATH = join(KILO_DIR, "skills");
20106
- const KILO_AGENTS_DIR_PATH = join(KILO_DIR, "agents");
20107
- const KILO_GLOBAL_AGENTS_DIR_PATH = join(KILO_GLOBAL_DIR, "agents");
20108
- const KILO_PLUGINS_DIR_PATH = join(KILO_DIR, "plugins");
20109
- const KILO_GLOBAL_PLUGINS_DIR_PATH = join(KILO_GLOBAL_DIR, "plugins");
20110
- const KILO_PLUGIN_DIR_PATH = join(KILO_DIR, "plugin");
20111
- const KILO_GLOBAL_PLUGIN_DIR_PATH = join(KILO_GLOBAL_DIR, "plugin");
20112
- const KILO_RULE_FILE_NAME = "AGENTS.md";
20113
- const KILO_IGNORE_FILE_NAME = ".kilocodeignore";
20114
- const KILO_JSONC_FILE_NAME = "kilo.jsonc";
20115
- const KILO_JSON_FILE_NAME = "kilo.json";
20116
- const KILO_HOOKS_FILE_NAME = "rulesync-hooks.js";
20117
- //#endregion
20118
20268
  //#region src/features/commands/kilo-command.ts
20119
20269
  const KiloCommandFrontmatterSchema = z.looseObject({
20120
20270
  description: z.optional(z.string()),
@@ -20608,9 +20758,26 @@ const OpenCodeCommandFrontmatterSchema = z.looseObject({
20608
20758
  description: z.optional(z.string()),
20609
20759
  agent: optional(z.string()),
20610
20760
  subtask: optional(z.boolean()),
20761
+ subagent: optional(z.boolean()),
20611
20762
  model: optional(z.string()),
20612
20763
  variant: optional(z.string())
20613
20764
  });
20765
+ /**
20766
+ * Rewrites a boolean `subagent` (OpenCode V2) into `subtask`, the key both V1
20767
+ * and V2 read: V1's command schema rejects unknown keys, while V2 resolves
20768
+ * `subagent ?? subtask`. `subagent` therefore wins when both are set, matching
20769
+ * V2. A non-boolean `subagent` is left untouched for validation to report.
20770
+ *
20771
+ * @see https://opencode.ai/v2/docs/commands/
20772
+ */
20773
+ function foldSubagentAlias(fields) {
20774
+ if (typeof fields.subagent !== "boolean") return fields;
20775
+ const { subagent, ...rest } = fields;
20776
+ return {
20777
+ ...rest,
20778
+ subtask: subagent
20779
+ };
20780
+ }
20614
20781
  var OpenCodeCommand = class extends ToolCommand {
20615
20782
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
20616
20783
  static layout = OPENCODE_LAYOUT;
@@ -20638,7 +20805,8 @@ var OpenCodeCommand = class extends ToolCommand {
20638
20805
  return this.frontmatter;
20639
20806
  }
20640
20807
  toRulesyncCommand() {
20641
- const { description, ...restFields } = this.frontmatter;
20808
+ const { description, ...toolFields } = this.frontmatter;
20809
+ const restFields = foldSubagentAlias(toolFields);
20642
20810
  const { toolTarget } = this.constructor.layout;
20643
20811
  const rulesyncFrontmatter = {
20644
20812
  targets: ["*"],
@@ -20658,7 +20826,7 @@ var OpenCodeCommand = class extends ToolCommand {
20658
20826
  }
20659
20827
  static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
20660
20828
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
20661
- const opencodeFields = rulesyncFrontmatter[this.layout.toolTarget] ?? {};
20829
+ const opencodeFields = foldSubagentAlias(rulesyncFrontmatter[this.layout.toolTarget] ?? {});
20662
20830
  const opencodeFrontmatter = {
20663
20831
  description: rulesyncFrontmatter.description,
20664
20832
  ...opencodeFields
@@ -20714,7 +20882,8 @@ var OpenCodeCommand = class extends ToolCommand {
20714
20882
  * Imports commands defined inline in `opencode.json` / `opencode.jsonc` under
20715
20883
  * the top-level `command` key (in addition to the Markdown files under
20716
20884
  * `.opencode/commands/`). Each entry's `template` becomes the command body,
20717
- * while `description` / `agent` / `model` / `subtask` map to the frontmatter.
20885
+ * while `description` / `agent` / `model` / `subtask` map to the frontmatter
20886
+ * (a V2 `subagent` is read as `subtask`, taking precedence like V2 does).
20718
20887
  *
20719
20888
  * Import-only: this is invoked by the commands processor when loading tool
20720
20889
  * files for conversion to rulesync, never for orphan deletion.
@@ -20734,11 +20903,12 @@ var OpenCodeCommand = class extends ToolCommand {
20734
20903
  const entry = asOpencodeEntries(rawEntry);
20735
20904
  if (!entry) continue;
20736
20905
  const body = typeof entry.template === "string" ? entry.template : "";
20906
+ const { subtask } = foldSubagentAlias(entry);
20737
20907
  const frontmatter = {
20738
20908
  ...typeof entry.description === "string" && { description: entry.description },
20739
20909
  ...typeof entry.agent === "string" && { agent: entry.agent },
20740
20910
  ...typeof entry.model === "string" && { model: entry.model },
20741
- ...typeof entry.subtask === "boolean" && { subtask: entry.subtask }
20911
+ ...typeof subtask === "boolean" && { subtask }
20742
20912
  };
20743
20913
  commands.push(new this({
20744
20914
  outputRoot,
@@ -20791,6 +20961,7 @@ const OMP_RULE_FILE_NAME = "AGENTS.md";
20791
20961
  const OMP_RULES_DIR_NAME = "rules";
20792
20962
  const OMP_AGENTS_DIR_NAME = "agents";
20793
20963
  const OMP_MCP_FILE_NAME = "mcp.json";
20964
+ const OMP_CONFIG_FILE_NAME = "config.yml";
20794
20965
  const OMP_LAYOUT = {
20795
20966
  toolTarget: "omp",
20796
20967
  dir: OMP_DIR,
@@ -22120,6 +22291,7 @@ function warpcliConfigDir() {
22120
22291
  }
22121
22292
  }
22122
22293
  const WARP_RULE_FILE_NAME = "AGENTS.md";
22294
+ const WARP_LEGACY_RULE_FILE_NAME = "WARP.md";
22123
22295
  const WARP_GLOBAL_RULE_DIR = ".agents";
22124
22296
  const WARP_MCP_FILE_NAME = ".mcp.json";
22125
22297
  const WARP_PERMISSIONS_FILE_NAME = "settings.toml";
@@ -35700,7 +35872,9 @@ var JunieIgnore = class JunieIgnore extends ToolIgnore {
35700
35872
  * KiloIgnore represents ignore patterns for the Kilo Code VSCode extension.
35701
35873
  *
35702
35874
  * Based on the Kilo Code specification:
35703
- * - File location: Workspace root folder only (.kilocodeignore)
35875
+ * - File location: workspace root (.kilocodeignore) for project scope, and
35876
+ * `~/.kilocode/.kilocodeignore` for global scope. Kilo merges the global
35877
+ * patterns first and the project ones after, so the project file wins.
35704
35878
  * - Syntax: Same as .gitignore
35705
35879
  * - Immediate reflection when saved
35706
35880
  * - Complete blocking of file access for ignored patterns
@@ -35708,11 +35882,12 @@ var JunieIgnore = class JunieIgnore extends ToolIgnore {
35708
35882
  *
35709
35883
  * Kilo reads `.kilocodeignore` (not `.kiloignore`), so emitting `.kiloignore`
35710
35884
  * left the file inert. https://kilo.ai/docs/customize/context/kilocodeignore
35885
+ * @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/ignore-migrator.ts
35711
35886
  */
35712
35887
  var KiloIgnore = class KiloIgnore extends ToolIgnore {
35713
- static getSettablePaths() {
35888
+ static getSettablePaths({ global = false } = {}) {
35714
35889
  return {
35715
- relativeDirPath: ".",
35890
+ relativeDirPath: global ? KILO_LEGACY_GLOBAL_DIR : ".",
35716
35891
  relativeFilePath: KILO_IGNORE_FILE_NAME
35717
35892
  };
35718
35893
  }
@@ -35725,35 +35900,40 @@ var KiloIgnore = class KiloIgnore extends ToolIgnore {
35725
35900
  /**
35726
35901
  * Create KiloIgnore from RulesyncIgnore
35727
35902
  */
35728
- static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
35903
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
35729
35904
  const body = rulesyncIgnore.getFileContent();
35905
+ const paths = this.getSettablePaths({ global });
35730
35906
  return new KiloIgnore({
35731
35907
  outputRoot,
35732
- relativeDirPath: this.getSettablePaths().relativeDirPath,
35733
- relativeFilePath: this.getSettablePaths().relativeFilePath,
35734
- fileContent: body
35908
+ relativeDirPath: paths.relativeDirPath,
35909
+ relativeFilePath: paths.relativeFilePath,
35910
+ fileContent: body,
35911
+ global
35735
35912
  });
35736
35913
  }
35737
35914
  /**
35738
35915
  * Load KiloIgnore from .kilocodeignore file
35739
35916
  */
35740
- static async fromFile({ outputRoot = process.cwd(), validate = true }) {
35741
- const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
35917
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
35918
+ const paths = this.getSettablePaths({ global });
35919
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
35742
35920
  return new KiloIgnore({
35743
35921
  outputRoot,
35744
- relativeDirPath: this.getSettablePaths().relativeDirPath,
35745
- relativeFilePath: this.getSettablePaths().relativeFilePath,
35922
+ relativeDirPath: paths.relativeDirPath,
35923
+ relativeFilePath: paths.relativeFilePath,
35746
35924
  fileContent,
35747
- validate
35925
+ validate,
35926
+ global
35748
35927
  });
35749
35928
  }
35750
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
35929
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
35751
35930
  return new KiloIgnore({
35752
35931
  outputRoot,
35753
35932
  relativeDirPath,
35754
35933
  relativeFilePath,
35755
35934
  fileContent: "",
35756
- validate: false
35935
+ validate: false,
35936
+ global
35757
35937
  });
35758
35938
  }
35759
35939
  };
@@ -36427,6 +36607,7 @@ const ignoreProcessorGlobalToolTargets = [
36427
36607
  "continue",
36428
36608
  "devin",
36429
36609
  "grokcli",
36610
+ "kilo",
36430
36611
  "kiro",
36431
36612
  "kiro-cli",
36432
36613
  "kiro-ide",
@@ -40288,6 +40469,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
40288
40469
  * `g` flag is required because callers may have multiple references per value.
40289
40470
  */
40290
40471
  const CANONICAL_ENV_VAR_PATTERN = /\$\{(?!env:)([^}:]+)\}/g;
40472
+ /**
40473
+ * OpenCode / Kilo `{env:VAR}` env var reference pattern. The negative lookbehind
40474
+ * avoids matching Cursor's `${env:VAR}` format.
40475
+ */
40476
+ const BRACE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
40291
40477
  function convertRecordValues({ record, pattern, replacement }) {
40292
40478
  return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, v.replace(pattern, replacement)]));
40293
40479
  }
@@ -40339,6 +40525,14 @@ function convertEnvVarRefsToToolFormat({ mcpServers, replacement }) {
40339
40525
  }) }
40340
40526
  }]));
40341
40527
  }
40528
+ /**
40529
+ * Names of the servers whose `env` or `headers` values carry at least one
40530
+ * canonical `${VAR}` env var reference.
40531
+ */
40532
+ function findServersWithEnvVarRefs(mcpServers) {
40533
+ const hasRef = (record) => Object.values(record ?? {}).some((value) => value.search(CANONICAL_ENV_VAR_PATTERN) !== -1);
40534
+ return Object.entries(mcpServers).filter(([, config]) => hasRef(config.env) || hasRef(config.headers)).map(([name]) => name);
40535
+ }
40342
40536
  //#endregion
40343
40537
  //#region src/features/mcp/cursor-mcp.ts
40344
40538
  const CURSOR_ENV_VAR_PATTERN = /\$\{env:([^}:]+)\}/g;
@@ -42627,17 +42821,50 @@ function readExistingKiloMcpEntries(fileContent) {
42627
42821
  }
42628
42822
  return entries;
42629
42823
  }
42630
- function convertToKiloFormat(mcpServers, existingMcp, logger) {
42824
+ const KILO_UNTRUSTED_ENV_REF_PATTERN = /\{env:[^}]+\}/;
42825
+ /**
42826
+ * Judged on the entry as written, not the canonical server: fields the entry
42827
+ * drops (a toggle keeps only `enabled`) cannot make Kilo reject the file.
42828
+ */
42829
+ function rejectUntrustedEnvRef(serverName, converted, rejectEnvRefs, logger) {
42830
+ if (!rejectEnvRefs || converted === null) return converted;
42831
+ if (!KILO_UNTRUSTED_ENV_REF_PATTERN.test(JSON.stringify(converted))) return converted;
42832
+ return warnAndSkipMcpServer({
42833
+ toolName: "Kilo",
42834
+ serverName,
42835
+ reason: "an {env:...} reference, which makes Kilo reject the whole project config",
42836
+ logger
42837
+ });
42838
+ }
42839
+ function convertToKiloFormat(mcpServers, existingMcp, logger, rejectEnvRefs = false) {
42631
42840
  const tools = {};
42632
42841
  return {
42633
42842
  mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
42634
- const converted = convertServerToKiloFormat(serverName, serverConfig, Object.hasOwn(existingMcp, serverName) ? existingMcp[serverName] : void 0, logger);
42843
+ const converted = rejectUntrustedEnvRef(serverName, convertServerToKiloFormat(serverName, serverConfig, Object.hasOwn(existingMcp, serverName) ? existingMcp[serverName] : void 0, logger), rejectEnvRefs, logger);
42635
42844
  collectKiloServerTools(tools, serverName, serverConfig);
42636
42845
  return converted === null ? null : [serverName, converted];
42637
42846
  }).filter((entry) => entry !== null)),
42638
42847
  tools
42639
42848
  };
42640
42849
  }
42850
+ /**
42851
+ * Kilo expands only `{env:VAR}`, never the canonical `${VAR}`, and only in a
42852
+ * trusted config: the global `~/.config/kilo/kilo.jsonc`. A project config is
42853
+ * untrusted, so any `{env:` in it makes Kilo drop the whole file (or, in MCP
42854
+ * headers, the server). In project scope canonical references are therefore
42855
+ * left as written — Kilo passes them through literally — with a warning (a
42856
+ * written entry already carrying `{env:` is skipped by `convertToKiloFormat`).
42857
+ * @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/config/variable.ts
42858
+ */
42859
+ function resolveKiloEnvVarRefs({ mcpServers, global, logger }) {
42860
+ if (global) return convertEnvVarRefsToToolFormat({
42861
+ mcpServers,
42862
+ replacement: "{env:$1}"
42863
+ });
42864
+ const serverNames = findServersWithEnvVarRefs(mcpServers);
42865
+ if (serverNames.length > 0) logger?.warn(`Kilo MCP servers ${serverNames.map((name) => `"${name}"`).join(", ")} use environment variable references (\${VAR}), which Kilo does not resolve in a project config; they are written literally. Use global mode (~/.config/kilo/kilo.jsonc) to have them expanded.`);
42866
+ return mcpServers;
42867
+ }
42641
42868
  var KiloMcp = class KiloMcp extends ToolMcp {
42642
42869
  json;
42643
42870
  constructor(params) {
@@ -42736,7 +42963,11 @@ var KiloMcp = class KiloMcp extends ToolMcp {
42736
42963
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
42737
42964
  }
42738
42965
  const existingMcp = readExistingKiloMcpEntries(fileContent);
42739
- const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(rulesyncMcp.getMcpServers(), existingMcp, logger);
42966
+ const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(resolveKiloEnvVarRefs({
42967
+ mcpServers: rulesyncMcp.getMcpServers(),
42968
+ global,
42969
+ logger
42970
+ }), existingMcp, logger, !global);
42740
42971
  return new KiloMcp({
42741
42972
  outputRoot,
42742
42973
  relativeDirPath: basePaths.relativeDirPath,
@@ -42802,7 +43033,10 @@ var KiloMcp = class KiloMcp extends ToolMcp {
42802
43033
  });
42803
43034
  }
42804
43035
  toRulesyncMcp() {
42805
- const { shared, toolOnly } = splitMcpServersByTransport(convertFromKiloFormat(this.json.mcp ?? {}, this.json.tools));
43036
+ const { shared, toolOnly } = splitMcpServersByTransport(convertEnvVarRefsFromToolFormat({
43037
+ mcpServers: convertFromKiloFormat(this.json.mcp ?? {}, this.json.tools),
43038
+ pattern: BRACE_ENV_VAR_PATTERN
43039
+ }));
42806
43040
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
42807
43041
  mcpServers: shared,
42808
43042
  ...Object.keys(toolOnly).length > 0 && { kilo: { mcpServers: toolOnly } }
@@ -43259,7 +43493,6 @@ var KiroMcp = class KiroMcp extends ToolMcp {
43259
43493
  };
43260
43494
  //#endregion
43261
43495
  //#region src/features/mcp/opencode-mcp.ts
43262
- const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
43263
43496
  const OpencodeMcpLocalServerSchema = z.looseObject({
43264
43497
  type: z.literal("local"),
43265
43498
  command: z.array(z.string()),
@@ -43642,7 +43875,7 @@ var OpencodeMcp = class extends ToolMcp {
43642
43875
  toRulesyncMcp() {
43643
43876
  const { shared, toolOnly } = splitMcpServersByTransport(convertEnvVarRefsFromToolFormat({
43644
43877
  mcpServers: convertFromOpencodeFormat(this.json.mcp ?? {}, this.json.tools),
43645
- pattern: OPENCODE_ENV_VAR_PATTERN
43878
+ pattern: BRACE_ENV_VAR_PATTERN
43646
43879
  }));
43647
43880
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
43648
43881
  mcpServers: shared,
@@ -44925,6 +45158,28 @@ const REASONIX_RETIRED_PLUGIN_FIELDS = ["trusted_read_only_tools"];
44925
45158
  function warnAboutRetiredFields({ name, serverRecord, logger }) {
44926
45159
  for (const field of REASONIX_RETIRED_PLUGIN_FIELDS) if (serverRecord[field] !== void 0) logger?.warn(`Reasonix MCP: dropping "${field}" from "${name}"; Reasonix retired the field in v1.17.18 and ignores it, so it is no longer written.`);
44927
45160
  }
45161
+ /**
45162
+ * The `concurrency` values Reasonix honors. It trims and lowercases before
45163
+ * comparing (`mcpServerIsSerial` in `internal/agent/mcp_concurrency.go`), so
45164
+ * `" Serial "` counts too and is written as authored.
45165
+ */
45166
+ const REASONIX_CONCURRENCY_VALUES = /* @__PURE__ */ new Set(["serial", "parallel"]);
45167
+ /**
45168
+ * Why a `concurrency` / `auto_start` value cannot be written, or `undefined`
45169
+ * when it can (and for every other field). A wrong type is the case that
45170
+ * matters: Reasonix declares the fields as `string` / `*bool`, and a TOML type
45171
+ * mismatch makes it refuse the whole config file. An unknown `concurrency`
45172
+ * string decodes fine but is ignored in favor of the server-name default, so it
45173
+ * is dropped too rather than written as a setting that does nothing.
45174
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/config/plugin_entry.go
45175
+ */
45176
+ function invalidSchedulingFieldReason(field, value) {
45177
+ if (field === "auto_start" && typeof value !== "boolean") return "Reasonix expects a boolean and fails to load a config file holding any other type.";
45178
+ if (field === "concurrency") {
45179
+ if (typeof value !== "string") return "Reasonix expects a string and fails to load a config file holding any other type.";
45180
+ if (!REASONIX_CONCURRENCY_VALUES.has(value.trim().toLowerCase())) return `Reasonix accepts only "serial" or "parallel" and ignores ${JSON.stringify(value)}, falling back to the server-name default.`;
45181
+ }
45182
+ }
44928
45183
  function rulesyncMcpServerToReasonix(name, server, logger) {
44929
45184
  const serverRecord = server;
44930
45185
  const type = resolveReasonixType(server);
@@ -44949,7 +45204,14 @@ function rulesyncMcpServerToReasonix(name, server, logger) {
44949
45204
  }
44950
45205
  for (const field of REASONIX_PLUGIN_FIELDS) {
44951
45206
  if (field === "type" || field === "command" || field === "args") continue;
44952
- if (serverRecord[field] !== void 0) plugin[field] = serverRecord[field];
45207
+ const value = serverRecord[field];
45208
+ if (value === void 0) continue;
45209
+ const invalidReason = invalidSchedulingFieldReason(field, value);
45210
+ if (invalidReason !== void 0) {
45211
+ logger?.warn(`Reasonix MCP: dropping "${field}" from "${name}"; ${invalidReason}`);
45212
+ continue;
45213
+ }
45214
+ plugin[field] = value;
44953
45215
  }
44954
45216
  warnAboutRetiredFields({
44955
45217
  name,
@@ -52016,7 +52278,7 @@ const CODEBUDDY_RULE_LISTS = [
52016
52278
  "ask",
52017
52279
  "deny"
52018
52280
  ];
52019
- const CATCH_ALL_PATTERN$10 = "*";
52281
+ const CATCH_ALL_PATTERN$11 = "*";
52020
52282
  const MCP_CATEGORY = "mcp";
52021
52283
  const MCP_RULE_PREFIX = "mcp__";
52022
52284
  const ALL_MCP_RULE = "mcp__*";
@@ -52089,7 +52351,7 @@ function categoryOfRule$2(rule) {
52089
52351
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
52090
52352
  */
52091
52353
  function buildCodebuddyRule({ category, pattern, action, logger }) {
52092
- const catchAll = pattern === CATCH_ALL_PATTERN$10;
52354
+ const catchAll = pattern === CATCH_ALL_PATTERN$11;
52093
52355
  if (category === MCP_CATEGORY) {
52094
52356
  if (catchAll && action === "allow") {
52095
52357
  logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
@@ -52098,7 +52360,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
52098
52360
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
52099
52361
  }
52100
52362
  if (category === "*") {
52101
- if (catchAll) return CATCH_ALL_PATTERN$10;
52363
+ if (catchAll) return CATCH_ALL_PATTERN$11;
52102
52364
  logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
52103
52365
  return null;
52104
52366
  }
@@ -52116,7 +52378,7 @@ function parseCodebuddyRule(rule) {
52116
52378
  const { specifier } = splitCodebuddyRule(rule);
52117
52379
  if (specifier === void 0) return {
52118
52380
  category,
52119
- pattern: CATCH_ALL_PATTERN$10
52381
+ pattern: CATCH_ALL_PATTERN$11
52120
52382
  };
52121
52383
  const trimmed = specifier.trim();
52122
52384
  return trimmed === "" ? null : {
@@ -52292,6 +52554,633 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
52292
52554
  }
52293
52555
  }
52294
52556
  //#endregion
52557
+ //#region src/features/permissions/codewhale-permissions.ts
52558
+ const CODEWHALE_SHELL_TOOL = "exec_shell";
52559
+ /**
52560
+ * Canonical path categories and the Codewhale policy tool names their rules are
52561
+ * written for. Codewhale consults typed rules only for `exec_shell` and these
52562
+ * file tools, so a rule for any other tool (`fetch_url`, `web_search`, an MCP
52563
+ * tool, ...) is never evaluated. `edit` covers `apply_patch` too: both modify
52564
+ * existing files, and an `edit` deny that left `apply_patch` open would not
52565
+ * hold.
52566
+ *
52567
+ * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/core/engine.rs (`file_tool_permission_paths`)
52568
+ * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/tools/canonical_action.rs
52569
+ */
52570
+ const CODEWHALE_PATH_CATEGORY_TOOLS = {
52571
+ read: ["read_file"],
52572
+ write: ["write_file"],
52573
+ edit: ["edit_file", "apply_patch"],
52574
+ grep: ["grep_files"],
52575
+ glob: ["file_search"],
52576
+ list: ["list_dir"]
52577
+ };
52578
+ /**
52579
+ * `apply_patch` can also create files (a `--- /dev/null` hunk), so a `write`
52580
+ * restriction is written for it too; a `write` allow is not, since it would
52581
+ * approve edits to existing files the `edit` category never allowed.
52582
+ *
52583
+ * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/tools/apply_patch.rs
52584
+ */
52585
+ const CODEWHALE_WRITE_RESTRICTION_TOOLS = ["write_file", "apply_patch"];
52586
+ /** The Codewhale tools a canonical path rule is written for. */
52587
+ function pathCategoryTools(category, action) {
52588
+ if (category === "write" && action !== "allow") return CODEWHALE_WRITE_RESTRICTION_TOOLS;
52589
+ return CODEWHALE_PATH_CATEGORY_TOOLS[category] ?? [];
52590
+ }
52591
+ /** The canonical path categories a Codewhale file tool's rule can come from. */
52592
+ const CODEWHALE_TOOL_TO_PATH_CATEGORIES = {
52593
+ read_file: ["read"],
52594
+ write_file: ["write"],
52595
+ edit_file: ["edit"],
52596
+ apply_patch: ["edit", "write"],
52597
+ grep_files: ["grep"],
52598
+ file_search: ["glob"],
52599
+ list_dir: ["list"]
52600
+ };
52601
+ const CODEWHALE_GLOBAL_ONLY_MESSAGE = "Codewhale permissions are global-only; use --global to sync ~/.codewhale/permissions.toml";
52602
+ const COMMAND_WILDCARD_CHARACTERS = /[*?[]/;
52603
+ const PATH_GLOB_CHARACTERS = /[*?[\]{}]/;
52604
+ function isCatchAllPattern(pattern) {
52605
+ return pattern === "*" || pattern === "**";
52606
+ }
52607
+ /**
52608
+ * The literal command prefix a wildcard pattern pins down, cut back to a word
52609
+ * boundary so it never names a partial word (`git push* --force` pins down
52610
+ * `git`, not `git push`). Empty when the pattern starts with a wildcard.
52611
+ */
52612
+ function literalCommandPrefix(pattern) {
52613
+ const firstWildcard = pattern.search(COMMAND_WILDCARD_CHARACTERS);
52614
+ const head = pattern.slice(0, firstWildcard);
52615
+ if (/\s$/.test(head)) return head.trim();
52616
+ const lastBoundary = head.search(/\s\S*$/);
52617
+ return lastBoundary < 0 ? "" : head.slice(0, lastBoundary).trim();
52618
+ }
52619
+ /**
52620
+ * Convert one canonical `bash` rule. Codewhale matches `command` as a
52621
+ * word-boundary prefix of the invocation (or the whole invocation with
52622
+ * `command_exact = true`); it expands no wildcards.
52623
+ *
52624
+ * - `*` / `**` → a tool-wide rule.
52625
+ * - `git *` → the prefix `git`.
52626
+ * - A wildcard-free `allow` → an exact command, so it approves nothing more
52627
+ * than the command it names. A wildcard-free `deny` / `ask` → a prefix, the
52628
+ * way Codewhale's own deny rules are matched, so the restriction also covers
52629
+ * the command with more arguments.
52630
+ * - Any other wildcard: a restricting rule is broadened to the literal prefix
52631
+ * it pins down; an `allow` is skipped, since no prefix approves only what it
52632
+ * names.
52633
+ */
52634
+ function convertShellRule({ pattern, action, source = SHELL_PERMISSION_CATEGORY, logger }) {
52635
+ const trimmed = pattern.trim();
52636
+ if (isCatchAllPattern(trimmed)) return {
52637
+ tool: CODEWHALE_SHELL_TOOL,
52638
+ action
52639
+ };
52640
+ if (trimmed === "") {
52641
+ logger?.warn(`Codewhale permissions: skipping empty ${source} ${action} pattern.`);
52642
+ return null;
52643
+ }
52644
+ if (!COMMAND_WILDCARD_CHARACTERS.test(trimmed)) return action === "allow" ? {
52645
+ tool: CODEWHALE_SHELL_TOOL,
52646
+ command: trimmed,
52647
+ command_exact: true,
52648
+ action
52649
+ } : {
52650
+ tool: CODEWHALE_SHELL_TOOL,
52651
+ command: trimmed,
52652
+ action
52653
+ };
52654
+ const trailingPrefix = trimmed.endsWith(" *") ? trimmed.slice(0, -2).trim() : null;
52655
+ if (trailingPrefix && !COMMAND_WILDCARD_CHARACTERS.test(trailingPrefix)) return {
52656
+ tool: CODEWHALE_SHELL_TOOL,
52657
+ command: trailingPrefix,
52658
+ action
52659
+ };
52660
+ if (action === "allow") {
52661
+ logger?.warn(`Codewhale permissions: skipping ${source} allow "${pattern}" because Codewhale matches commands by prefix and expands no wildcards.`);
52662
+ return null;
52663
+ }
52664
+ const prefix = literalCommandPrefix(trimmed);
52665
+ if (prefix === "") {
52666
+ logger?.warn(`Codewhale permissions: skipping ${source} ${action} "${pattern}" because it pins down no literal command prefix Codewhale could match.`);
52667
+ return null;
52668
+ }
52669
+ logger?.warn(`Codewhale permissions: broadening ${source} ${action} "${pattern}" to the command prefix "${prefix}" because Codewhale expands no wildcards.`);
52670
+ return {
52671
+ tool: CODEWHALE_SHELL_TOOL,
52672
+ command: prefix,
52673
+ action
52674
+ };
52675
+ }
52676
+ /**
52677
+ * Convert one canonical path rule. Codewhale matches `path` exactly (after
52678
+ * normalizing it against the workspace) and expands no globs, so `*` / `**`
52679
+ * become a tool-wide rule and any other glob is skipped: narrowing it to a
52680
+ * single path would leave the rest of the glob unrestricted, and widening it to
52681
+ * the whole tool would restrict far more than written.
52682
+ */
52683
+ function convertPathRules({ category, pattern, action, logger }) {
52684
+ const tools = pathCategoryTools(category, action);
52685
+ const trimmed = pattern.trim();
52686
+ if (isCatchAllPattern(trimmed)) return tools.map((tool) => ({
52687
+ tool,
52688
+ action
52689
+ }));
52690
+ if (trimmed === "" || PATH_GLOB_CHARACTERS.test(trimmed)) {
52691
+ logger?.warn(`Codewhale permissions: skipping ${category} ${action} "${pattern}" because Codewhale matches exact paths and expands no globs.`);
52692
+ return [];
52693
+ }
52694
+ return tools.map((tool) => ({
52695
+ tool,
52696
+ path: trimmed,
52697
+ action
52698
+ }));
52699
+ }
52700
+ function ruleKey(rule) {
52701
+ return JSON.stringify([
52702
+ rule.tool,
52703
+ rule.command ?? null,
52704
+ rule.command_exact ?? false,
52705
+ rule.path ?? null,
52706
+ rule.workspace ?? null,
52707
+ rule.action ?? "ask"
52708
+ ]);
52709
+ }
52710
+ function withExplicitAction(rule) {
52711
+ return {
52712
+ ...rule,
52713
+ action: rule.action ?? "ask"
52714
+ };
52715
+ }
52716
+ /**
52717
+ * Codewhale picks the matching rule by action (`deny` > `ask` > `allow`) before
52718
+ * specificity, so file order never changes a decision. The rules are still
52719
+ * sorted that way, then by tool, so the output is stable and reads in the order
52720
+ * Codewhale applies it.
52721
+ */
52722
+ function sortRules(rules) {
52723
+ return rules.map((rule, index) => ({
52724
+ rule,
52725
+ index
52726
+ })).toSorted((left, right) => {
52727
+ const actionOrder = PERMISSION_ACTION_PRIORITY[right.rule.action] - PERMISSION_ACTION_PRIORITY[left.rule.action];
52728
+ if (actionOrder !== 0) return actionOrder;
52729
+ const toolOrder = left.rule.tool.localeCompare(right.rule.tool);
52730
+ if (toolOrder !== 0) return toolOrder;
52731
+ return left.index - right.index;
52732
+ }).map(({ rule }) => rule);
52733
+ }
52734
+ function recordDroppedRestriction({ dropped, tools, glob }) {
52735
+ for (const tool of tools) dropped.set(tool, [...dropped.get(tool) ?? [], glob]);
52736
+ }
52737
+ /**
52738
+ * A `[...]` class in a restriction widens to `?` (any one character): matched
52739
+ * after lowercasing, a negated or mixed-case class would otherwise narrow what
52740
+ * the restriction covers.
52741
+ */
52742
+ function widenRestrictionClasses(pattern) {
52743
+ return pattern.replace(/\[[!^]?\]?[^\]]*\]/g, "?");
52744
+ }
52745
+ /**
52746
+ * An `allow` rule names a literal command or path, so a `[` in it widens to `?`
52747
+ * rather than opening a class that would not even match the rule itself.
52748
+ */
52749
+ function literalAsGlob(value) {
52750
+ return value.replaceAll("[", "?");
52751
+ }
52752
+ /**
52753
+ * Codewhale matches commands case-insensitively and word by word, so commands
52754
+ * are compared lowercased with their whitespace collapsed.
52755
+ */
52756
+ function normalizeCommandGlob(glob) {
52757
+ return glob.trim().replace(/\s+/g, " ").toLowerCase();
52758
+ }
52759
+ /**
52760
+ * Codewhale normalizes a rule's path against the workspace, dropping empty and
52761
+ * `.` segments, so the same file can be spelled several ways. Paths are
52762
+ * compared lowercased (for case-insensitive file systems) with those segments
52763
+ * dropped, a `{a,b}` group widens to `*`, a `**` segment widens so it also
52764
+ * matches no directory at all (a leading `**` segment before `*.pem` reaches
52765
+ * `dev.pem`, and `secrets/**` reaches `secrets`), and a path that is absolute,
52766
+ * home-relative or climbs with `..` widens to `*` outright, since it cannot be
52767
+ * compared against a workspace-relative one. Every rewrite only widens a
52768
+ * pattern, so an inexact comparison withholds an `allow` rather than writing
52769
+ * one a dropped restriction overlaps.
52770
+ */
52771
+ function normalizePathGlob(glob) {
52772
+ const normalized = glob.trim().replaceAll("\\", "/").toLowerCase();
52773
+ if (/^(?:\/|~|[a-z]:)/.test(normalized) || /(?:^|\/)\.\.(?:\/|$)/.test(normalized)) return "*";
52774
+ return normalized.split("/").filter((segment) => segment !== "" && segment !== ".").join("/").replace(/\{.*\}/g, "*").replace(/\*\*\//g, "*").replace(/\/\*\*$/, "*");
52775
+ }
52776
+ /** A dropped `bash` (or all-tools) restriction, as a glob to compare `allow` rules against. */
52777
+ function shellRestrictionGlob(pattern) {
52778
+ return normalizeCommandGlob(widenRestrictionClasses(pattern));
52779
+ }
52780
+ /** A dropped path restriction, as a glob to compare `allow` rules against. */
52781
+ function pathRestrictionGlob(pattern) {
52782
+ return normalizePathGlob(widenRestrictionClasses(pattern));
52783
+ }
52784
+ /**
52785
+ * The file tools whose `path` is the directory a search or listing starts from,
52786
+ * so an `allow` on a directory reaches everything under it.
52787
+ */
52788
+ const CODEWHALE_DIRECTORY_ROOTED_TOOLS = /* @__PURE__ */ new Set([
52789
+ "grep_files",
52790
+ "file_search",
52791
+ "list_dir"
52792
+ ]);
52793
+ /**
52794
+ * The globs a written `allow` rule approves, for comparison with dropped
52795
+ * restrictions. A path that normalizes to the workspace root (`.`) approves
52796
+ * everything, and a directory-rooted tool's path approves what lies under it.
52797
+ */
52798
+ function allowRuleComparisonGlobs(rule) {
52799
+ if (rule.tool === CODEWHALE_SHELL_TOOL) {
52800
+ if (rule.command === void 0) return ["*"];
52801
+ const command = normalizeCommandGlob(literalAsGlob(rule.command));
52802
+ return rule.command_exact ? [command] : [command, `${command} *`];
52803
+ }
52804
+ const path = rule.path === void 0 ? "" : normalizePathGlob(literalAsGlob(rule.path));
52805
+ if (path === "") return ["*"];
52806
+ return CODEWHALE_DIRECTORY_ROOTED_TOOLS.has(rule.tool) ? [path, `${path}/*`] : [path];
52807
+ }
52808
+ /**
52809
+ * Drop every `allow` rule that a restriction which could not be written
52810
+ * overlaps (any one command or path matches both), so what the restriction
52811
+ * named reaches Codewhale's approval prompt instead of being auto-approved. An
52812
+ * `allow` no dropped restriction overlaps is kept. Comparisons share one budget;
52813
+ * once it runs out every comparison answers "overlaps", so the rule is withheld.
52814
+ */
52815
+ function withholdShadowedAllowRules({ rules, dropped, logger }) {
52816
+ const budget = createIntersectionBudget();
52817
+ const tests = new Map([...dropped].map(([tool, globs]) => [tool, createShadowingRestrictionsTest(globs.map((pattern) => ({
52818
+ pattern,
52819
+ fromAllToolsCategory: false
52820
+ })), { budget })]));
52821
+ return rules.filter((rule) => {
52822
+ const test = tests.get(rule.tool);
52823
+ if ((rule.action ?? "ask") !== "allow" || test === void 0) return true;
52824
+ const shadowing = [...new Set(allowRuleComparisonGlobs(rule).flatMap((glob) => test(glob)))];
52825
+ if (shadowing.length === 0) return true;
52826
+ logger?.warn(`Codewhale permissions: withholding ${rule.tool} allow ${JSON.stringify(rule.command ?? rule.path ?? "*")}${rule.workspace === void 0 ? "" : ` (workspace ${rule.workspace})`} because the deny/ask rule(s) ${shadowing.map((pattern) => JSON.stringify(pattern)).join(", ")} for ${rule.tool} could not be written; those calls keep Codewhale's approval prompt.`);
52827
+ return false;
52828
+ });
52829
+ }
52830
+ /**
52831
+ * An all-tools `deny` / `ask` names paths as much as commands (`secrets/**`
52832
+ * under `*` denies a path), so it is written for every file tool as well. A
52833
+ * pattern Codewhale cannot match as a path is recorded as dropped for every file
52834
+ * tool instead, the same way a skipped restriction of a path category is.
52835
+ */
52836
+ function convertAllToolsPathRestriction({ pattern, action, converted, dropped, logger }) {
52837
+ const categories = Object.keys(CODEWHALE_PATH_CATEGORY_TOOLS);
52838
+ const pathRules = categories.flatMap((category) => convertPathRules({
52839
+ category,
52840
+ pattern,
52841
+ action
52842
+ }));
52843
+ if (pathRules.length > 0) {
52844
+ converted.push(...pathRules);
52845
+ return;
52846
+ }
52847
+ logger?.warn(`Codewhale permissions: all-tools ${action} "${pattern}" cannot be written for Codewhale's file tools, which match exact paths and expand no globs.`);
52848
+ for (const category of categories) recordDroppedRestriction({
52849
+ dropped,
52850
+ tools: pathCategoryTools(category, action),
52851
+ glob: pathRestrictionGlob(pattern)
52852
+ });
52853
+ }
52854
+ /**
52855
+ * Convert the rules that govern shell commands (`bash`, plus the restricting
52856
+ * rules of the all-tools `*` category) into `converted`, recording in
52857
+ * `dropped` each restriction that could not be written.
52858
+ */
52859
+ function convertShellCommandRules({ config, converted, dropped, logger }) {
52860
+ const { rules: shellRules, ignoredAllToolsAllowPatterns } = collectShellCommandRules(config.permission);
52861
+ for (const { pattern, action, fromAllToolsCategory } of shellRules) {
52862
+ const rule = convertShellRule({
52863
+ pattern,
52864
+ action,
52865
+ source: fromAllToolsCategory ? "all-tools (as a shell command)" : SHELL_PERMISSION_CATEGORY,
52866
+ logger
52867
+ });
52868
+ if (rule) converted.push(rule);
52869
+ else if (action !== "allow") recordDroppedRestriction({
52870
+ dropped,
52871
+ tools: [CODEWHALE_SHELL_TOOL],
52872
+ glob: shellRestrictionGlob(pattern)
52873
+ });
52874
+ if (fromAllToolsCategory) convertAllToolsPathRestriction({
52875
+ pattern,
52876
+ action,
52877
+ converted,
52878
+ dropped,
52879
+ logger
52880
+ });
52881
+ }
52882
+ if (ignoredAllToolsAllowPatterns.length > 0) logger?.warn(`Codewhale permissions: skipping all-tools allow rule(s) ${ignoredAllToolsAllowPatterns.map((pattern) => `"${pattern}"`).join(", ")}; an allow under "*" is never widened to Codewhale's tools.`);
52883
+ }
52884
+ /**
52885
+ * Convert the canonical block. The all-tools `*` category contributes its
52886
+ * restricting rules to shell commands (as every command-only adapter reads it,
52887
+ * see `collectShellCommandRules`), and its catch-all `*` restriction to every
52888
+ * file tool as well.
52889
+ *
52890
+ * A `deny` / `ask` Codewhale cannot express is not merely skipped: every
52891
+ * `allow` rule it overlaps for a tool it was meant to restrict is withheld too —
52892
+ * the generated ones and the preserved workspace-scoped grants alike — so the
52893
+ * commands or paths it named reach Codewhale's approval prompt instead of being
52894
+ * auto-approved by a broader `allow` (`{ "*": "allow", "secrets/**": "deny" }`
52895
+ * must not become a bare tool-wide allow). The `codewhale.rules` passthrough is
52896
+ * written as it stands.
52897
+ */
52898
+ function canonicalToCodewhaleRules({ config, preservedRules, logger }) {
52899
+ const converted = [];
52900
+ const dropped = /* @__PURE__ */ new Map();
52901
+ convertShellCommandRules({
52902
+ config,
52903
+ converted,
52904
+ dropped,
52905
+ logger
52906
+ });
52907
+ for (const [category, rules] of Object.entries(config.permission)) {
52908
+ if (category === "bash" || category === "*") continue;
52909
+ if (CODEWHALE_PATH_CATEGORY_TOOLS[category] === void 0) {
52910
+ if (Object.keys(rules).length > 0) logger?.warn(`Codewhale permissions: skipping unsupported category "${category}"; Codewhale evaluates permission rules only for shell commands and file tools.`);
52911
+ continue;
52912
+ }
52913
+ for (const [pattern, action] of Object.entries(rules)) {
52914
+ const pathRules = convertPathRules({
52915
+ category,
52916
+ pattern,
52917
+ action,
52918
+ logger
52919
+ });
52920
+ converted.push(...pathRules);
52921
+ if (pathRules.length === 0 && action !== "allow") recordDroppedRestriction({
52922
+ dropped,
52923
+ tools: pathCategoryTools(category, action),
52924
+ glob: pathRestrictionGlob(pattern)
52925
+ });
52926
+ }
52927
+ }
52928
+ const kept = withholdShadowedAllowRules({
52929
+ rules: converted,
52930
+ dropped,
52931
+ logger
52932
+ });
52933
+ const keptPreserved = withholdShadowedAllowRules({
52934
+ rules: preservedRules,
52935
+ dropped,
52936
+ logger
52937
+ });
52938
+ return [
52939
+ ...(config.codewhale?.rules ?? []).map(withExplicitAction),
52940
+ ...sortRules(kept),
52941
+ ...keptPreserved
52942
+ ];
52943
+ }
52944
+ function parsePermissionsDocument({ fileContent, filePath }) {
52945
+ if (fileContent.trim() === "") return {};
52946
+ try {
52947
+ return smolToml.parse(fileContent);
52948
+ } catch (error) {
52949
+ throw new Error(`Failed to parse existing Codewhale permissions at ${filePath}: ${formatError(error)}`, { cause: error });
52950
+ }
52951
+ }
52952
+ /**
52953
+ * The workspace-scoped rules already in the file. Codewhale's approval card
52954
+ * appends one ("Always allow this exact rule in this repo") with the absolute
52955
+ * `workspace` it was granted in, and rulesync never derives a `workspace` from
52956
+ * the canonical block, so these are remembered grants rather than earlier
52957
+ * rulesync output and survive a regenerate. A record Codewhale would reject is
52958
+ * not carried over, since it would make Codewhale refuse the whole file.
52959
+ */
52960
+ function collectWorkspaceScopedRules({ document, logger }) {
52961
+ const { valid, invalidCount } = partitionRuleRecords(document);
52962
+ if (invalidCount > 0) logger?.warn(`Codewhale permissions: dropping ${invalidCount} existing rule record(s) Codewhale would reject (an unknown key or a wrong type).`);
52963
+ return valid.filter((rule) => rule.workspace !== void 0);
52964
+ }
52965
+ /**
52966
+ * Codewhale reads `permissions.toml` next to the `config.toml` in use, which
52967
+ * `CODEWHALE_HOME` or `CODEWHALE_CONFIG_PATH` can move away from
52968
+ * `~/.codewhale/`. rulesync always writes `~/.codewhale/permissions.toml`, so
52969
+ * say so when either is set rather than let the rules go unread.
52970
+ */
52971
+ function warnAboutRelocatedConfig(logger) {
52972
+ for (const name of ["CODEWHALE_HOME", "CODEWHALE_CONFIG_PATH"]) if (process.env[name]) logger?.warn(`Codewhale permissions: ${name} is set, but rulesync only syncs ~/.codewhale/permissions.toml; Codewhale reads permissions.toml next to the config.toml in use.`);
52973
+ }
52974
+ /** The records of a parsed file that Codewhale itself would accept. */
52975
+ function partitionRuleRecords(document) {
52976
+ const rawRules = Array.isArray(document.rules) ? document.rules : [];
52977
+ const valid = [];
52978
+ let invalidCount = 0;
52979
+ for (const raw of rawRules) {
52980
+ const parsed = CodewhalePermissionRuleSchema.safeParse(raw);
52981
+ if (parsed.success) valid.push(parsed.data);
52982
+ else invalidCount += 1;
52983
+ }
52984
+ return {
52985
+ valid,
52986
+ invalidCount
52987
+ };
52988
+ }
52989
+ /**
52990
+ * The canonical `bash` pattern a native shell rule round-trips through, or
52991
+ * `null` when the next generate would not write the same rule back from it.
52992
+ */
52993
+ function shellRuleToPattern(rule) {
52994
+ if (rule.path !== void 0) return null;
52995
+ if (rule.command === void 0) return rule.command_exact ? null : "*";
52996
+ const command = rule.command.trim();
52997
+ if (command === "" || COMMAND_WILDCARD_CHARACTERS.test(command)) return null;
52998
+ if (rule.command_exact) return rule.action === "allow" ? command : null;
52999
+ return `${command} *`;
53000
+ }
53001
+ function pathRuleToPattern(rule) {
53002
+ if (rule.command !== void 0 || rule.command_exact) return null;
53003
+ if (rule.path === void 0) return "*";
53004
+ const path = rule.path.trim();
53005
+ return path === "" || PATH_GLOB_CHARACTERS.test(path) ? null : path;
53006
+ }
53007
+ function toImportCandidates(rule) {
53008
+ if (rule.workspace !== void 0) return [];
53009
+ if (rule.tool === CODEWHALE_SHELL_TOOL) {
53010
+ const pattern = shellRuleToPattern(rule);
53011
+ return pattern === null ? [] : [{
53012
+ category: SHELL_PERMISSION_CATEGORY,
53013
+ pattern,
53014
+ rule
53015
+ }];
53016
+ }
53017
+ const pattern = pathRuleToPattern(rule);
53018
+ if (pattern === null) return [];
53019
+ return (CODEWHALE_TOOL_TO_PATH_CATEGORIES[rule.tool] ?? []).map((category) => ({
53020
+ category,
53021
+ pattern,
53022
+ rule
53023
+ }));
53024
+ }
53025
+ /**
53026
+ * Split the native rules into the canonical block and the `codewhale.rules`
53027
+ * passthrough so that the next generate writes the same file back. A canonical
53028
+ * pattern is taken only when every tool its category expands to carries the
53029
+ * same rule (`edit` needs both `edit_file` and `apply_patch`, a `write`
53030
+ * restriction both `write_file` and `apply_patch`). A rule no canonical pattern
53031
+ * claims — including the weaker of two rules for one pattern — stays native.
53032
+ */
53033
+ function codewhaleRulesToCanonical(rules) {
53034
+ const permission = {};
53035
+ const claimedIndexes = /* @__PURE__ */ new Set();
53036
+ const grouped = /* @__PURE__ */ new Map();
53037
+ for (const [index, rule] of rules.entries()) for (const candidate of toImportCandidates(rule)) {
53038
+ const key = JSON.stringify([
53039
+ candidate.category,
53040
+ candidate.pattern,
53041
+ rule.action
53042
+ ]);
53043
+ grouped.set(key, [...grouped.get(key) ?? [], {
53044
+ ...candidate,
53045
+ index
53046
+ }]);
53047
+ }
53048
+ const strongestFirst = [...grouped.values()].toSorted((left, right) => PERMISSION_ACTION_PRIORITY[right[0].rule.action] - PERMISSION_ACTION_PRIORITY[left[0].rule.action]);
53049
+ for (const group of strongestFirst) {
53050
+ const { category, pattern, rule } = group[0];
53051
+ const expectedTools = category === "bash" ? [CODEWHALE_SHELL_TOOL] : pathCategoryTools(category, rule.action);
53052
+ const presentTools = new Set(group.map((candidate) => candidate.rule.tool));
53053
+ const complete = expectedTools.every((tool) => presentTools.has(tool));
53054
+ const categoryRules = permission[category] ?? {};
53055
+ if (!complete || Object.hasOwn(categoryRules, pattern)) continue;
53056
+ permission[category] = {
53057
+ ...categoryRules,
53058
+ [pattern]: rule.action
53059
+ };
53060
+ for (const candidate of group) if (expectedTools.includes(candidate.rule.tool)) claimedIndexes.add(candidate.index);
53061
+ }
53062
+ return {
53063
+ permission,
53064
+ nativeRules: rules.filter((_rule, index) => !claimedIndexes.has(index))
53065
+ };
53066
+ }
53067
+ /**
53068
+ * Codewhale permission rules in `~/.codewhale/permissions.toml`.
53069
+ *
53070
+ * The file holds a `rules` array of typed records (`tool`, optional `command`
53071
+ * prefix or exact `path`, optional `command_exact`, optional absolute
53072
+ * `workspace`, and `action`). It is the only permission-rule source Codewhale
53073
+ * reads — the project `.codewhale/config.toml` overlay cannot carry rules — so
53074
+ * the feature is global-only.
53075
+ *
53076
+ * rulesync owns the file's rules, except for workspace-scoped ones: those are
53077
+ * the grants Codewhale's approval card remembers per repository, and they are
53078
+ * kept across a regenerate. Codewhale also appends plain `exec_shell` ask rules
53079
+ * from the approval card (`S`), which cannot be told apart from rulesync's own
53080
+ * output and are replaced; run an import first to keep them.
53081
+ *
53082
+ * @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
53083
+ * @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
53084
+ */
53085
+ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
53086
+ constructor(params) {
53087
+ super({
53088
+ ...params,
53089
+ fileContent: params.fileContent ?? ""
53090
+ });
53091
+ }
53092
+ /**
53093
+ * Codewhale's approval card writes to the same file, so this feature never
53094
+ * deletes it.
53095
+ */
53096
+ isDeletable() {
53097
+ return false;
53098
+ }
53099
+ shouldSkipCreationWhenPayloadEmpty() {
53100
+ return true;
53101
+ }
53102
+ static getSettablePaths(_options) {
53103
+ return {
53104
+ relativeDirPath: CODEWHALE_DIR,
53105
+ relativeFilePath: CODEWHALE_PERMISSIONS_FILE_NAME
53106
+ };
53107
+ }
53108
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
53109
+ if (!global) throw new Error(CODEWHALE_GLOBAL_ONLY_MESSAGE);
53110
+ warnAboutRelocatedConfig(logger);
53111
+ const paths = CodewhalePermissions.getSettablePaths();
53112
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
53113
+ const fileContent = await readFileContentOrNull(filePath) ?? "";
53114
+ const { invalidCount } = partitionRuleRecords(parsePermissionsDocument({
53115
+ fileContent,
53116
+ filePath
53117
+ }));
53118
+ if (invalidCount > 0) logger?.warn(`Codewhale permissions: skipping ${invalidCount} rule record(s) in ${filePath} that Codewhale would reject (an unknown key or a wrong type).`);
53119
+ return new CodewhalePermissions({
53120
+ outputRoot,
53121
+ relativeDirPath: paths.relativeDirPath,
53122
+ relativeFilePath: paths.relativeFilePath,
53123
+ fileContent,
53124
+ validate,
53125
+ global: true
53126
+ });
53127
+ }
53128
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
53129
+ if (!global) throw new Error(CODEWHALE_GLOBAL_ONLY_MESSAGE);
53130
+ warnAboutRelocatedConfig(logger);
53131
+ const paths = CodewhalePermissions.getSettablePaths();
53132
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
53133
+ const existing = parsePermissionsDocument({
53134
+ fileContent: await readFileContentOrNull(filePath) ?? "",
53135
+ filePath
53136
+ });
53137
+ const generated = canonicalToCodewhaleRules({
53138
+ config: rulesyncPermissions.getJson(),
53139
+ preservedRules: collectWorkspaceScopedRules({
53140
+ document: existing,
53141
+ logger
53142
+ }),
53143
+ logger
53144
+ });
53145
+ const seen = /* @__PURE__ */ new Set();
53146
+ const rules = [];
53147
+ for (const rule of generated) {
53148
+ const key = ruleKey(rule);
53149
+ if (seen.has(key)) continue;
53150
+ seen.add(key);
53151
+ rules.push(rule);
53152
+ }
53153
+ return new CodewhalePermissions({
53154
+ outputRoot,
53155
+ relativeDirPath: paths.relativeDirPath,
53156
+ relativeFilePath: paths.relativeFilePath,
53157
+ fileContent: rules.length > 0 ? smolToml.stringify({ rules }) : "",
53158
+ validate: true,
53159
+ global: true
53160
+ });
53161
+ }
53162
+ toRulesyncPermissions() {
53163
+ const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
53164
+ fileContent: this.getFileContent(),
53165
+ filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
53166
+ })).valid.map(withExplicitAction));
53167
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
53168
+ permission,
53169
+ ...nativeRules.length > 0 && { codewhale: { rules: nativeRules } }
53170
+ }, null, 2) });
53171
+ }
53172
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
53173
+ return new CodewhalePermissions({
53174
+ outputRoot,
53175
+ relativeDirPath,
53176
+ relativeFilePath,
53177
+ fileContent: "",
53178
+ validate: false,
53179
+ global: true
53180
+ });
53181
+ }
53182
+ };
53183
+ //#endregion
52295
53184
  //#region src/features/permissions/codexcli-permissions.ts
52296
53185
  const RULESYNC_PROFILE_NAME = "rulesync";
52297
53186
  const CODEX_WORKSPACE_ROOTS_KEY = ":workspace_roots";
@@ -53167,7 +54056,7 @@ function mapBashActionToDecision(action) {
53167
54056
  //#region src/features/permissions/commandcode-permissions.ts
53168
54057
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
53169
54058
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
53170
- const CATCH_ALL_PATTERN$9 = "*";
54059
+ const CATCH_ALL_PATTERN$10 = "*";
53171
54060
  const MCP_CANONICAL_PREFIX$6 = "mcp__";
53172
54061
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
53173
54062
  const CATEGORY_TO_COMMANDCODE_TOOL = {
@@ -53222,10 +54111,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
53222
54111
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
53223
54112
  */
53224
54113
  function buildCommandcodeRule(category, pattern) {
53225
- const catchAll = pattern === CATCH_ALL_PATTERN$9 || pattern === "";
54114
+ const catchAll = pattern === CATCH_ALL_PATTERN$10 || pattern === "";
53226
54115
  if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
53227
54116
  if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
53228
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$9 : null;
54117
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$10 : null;
53229
54118
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
53230
54119
  if (tool === void 0) return null;
53231
54120
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -53261,7 +54150,7 @@ function splitCommandcodeRule(rule) {
53261
54150
  }
53262
54151
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
53263
54152
  function isCatchAllSpecifier(inner) {
53264
- return inner === "" || inner === CATCH_ALL_PATTERN$9;
54153
+ return inner === "" || inner === CATCH_ALL_PATTERN$10;
53265
54154
  }
53266
54155
  /**
53267
54156
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -53295,34 +54184,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
53295
54184
  if (server.length === 0) return null;
53296
54185
  const everyServer = {
53297
54186
  category: "mcp",
53298
- pattern: CATCH_ALL_PATTERN$9,
54187
+ pattern: CATCH_ALL_PATTERN$10,
53299
54188
  notInAllow: true
53300
54189
  };
53301
54190
  if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
53302
- if (server === CATCH_ALL_PATTERN$9) return everyServer;
53303
- if (server.includes(CATCH_ALL_PATTERN$9)) return null;
54191
+ if (server === CATCH_ALL_PATTERN$10) return everyServer;
54192
+ if (server.includes(CATCH_ALL_PATTERN$10)) return null;
53304
54193
  return {
53305
- category: rest === "" || rest === CATCH_ALL_PATTERN$9 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
53306
- pattern: CATCH_ALL_PATTERN$9,
54194
+ category: rest === "" || rest === CATCH_ALL_PATTERN$10 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
54195
+ pattern: CATCH_ALL_PATTERN$10,
53307
54196
  notInAllow: false
53308
54197
  };
53309
54198
  }
53310
54199
  const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
53311
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$9);
54200
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$10);
53312
54201
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
53313
54202
  category,
53314
54203
  pattern: inner,
53315
54204
  notInAllow: true
53316
54205
  } : null;
53317
- if (remainder === CATCH_ALL_PATTERN$9) return everyServer;
53318
- if (rest === CATCH_ALL_PATTERN$9 && !server.includes(CATCH_ALL_PATTERN$9)) return {
54206
+ if (remainder === CATCH_ALL_PATTERN$10) return everyServer;
54207
+ if (rest === CATCH_ALL_PATTERN$10 && !server.includes(CATCH_ALL_PATTERN$10)) return {
53319
54208
  category: `${MCP_CANONICAL_PREFIX$6}${server}`,
53320
- pattern: CATCH_ALL_PATTERN$9,
54209
+ pattern: CATCH_ALL_PATTERN$10,
53321
54210
  notInAllow: true
53322
54211
  };
53323
54212
  return namesTool ? {
53324
54213
  category,
53325
- pattern: CATCH_ALL_PATTERN$9,
54214
+ pattern: CATCH_ALL_PATTERN$10,
53326
54215
  notInAllow: false
53327
54216
  } : null;
53328
54217
  }
@@ -53346,14 +54235,14 @@ function friendlyToolRule({ category, inner }) {
53346
54235
  if (category === "bash") {
53347
54236
  if (isCatchAllSpecifier(inner)) return {
53348
54237
  category,
53349
- pattern: CATCH_ALL_PATTERN$9,
54238
+ pattern: CATCH_ALL_PATTERN$10,
53350
54239
  notInAllow: false
53351
54240
  };
53352
54241
  const normalized = inner.trim().replace(/\s+/g, " ");
53353
54242
  if (normalized === "" || normalized === ":*") return null;
53354
54243
  return isCatchAllSpecifier(normalized) ? {
53355
54244
  category,
53356
- pattern: CATCH_ALL_PATTERN$9,
54245
+ pattern: CATCH_ALL_PATTERN$10,
53357
54246
  notInAllow: true
53358
54247
  } : {
53359
54248
  category,
@@ -53363,7 +54252,7 @@ function friendlyToolRule({ category, inner }) {
53363
54252
  }
53364
54253
  if (isCatchAllSpecifier(inner)) return {
53365
54254
  category,
53366
- pattern: CATCH_ALL_PATTERN$9,
54255
+ pattern: CATCH_ALL_PATTERN$10,
53367
54256
  notInAllow: false
53368
54257
  };
53369
54258
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -53382,9 +54271,9 @@ function friendlyToolRule({ category, inner }) {
53382
54271
  */
53383
54272
  function parseCommandcodeRule(rule) {
53384
54273
  const { tool, inner } = splitCommandcodeRule(rule);
53385
- if (tool === CATCH_ALL_PATTERN$9) return isCatchAllSpecifier(inner) ? {
54274
+ if (tool === CATCH_ALL_PATTERN$10) return isCatchAllSpecifier(inner) ? {
53386
54275
  category: "*",
53387
- pattern: CATCH_ALL_PATTERN$9,
54276
+ pattern: CATCH_ALL_PATTERN$10,
53388
54277
  notInAllow: true
53389
54278
  } : null;
53390
54279
  const lowered = tool.toLowerCase();
@@ -53454,7 +54343,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
53454
54343
  }).filter((rule) => {
53455
54344
  const parsed = parseCommandcodeRule(rule);
53456
54345
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
53457
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$9})`);
54346
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$10})`);
53458
54347
  if (key !== "allow" && isStricterAction({
53459
54348
  action: key,
53460
54349
  existing: replacement
@@ -53511,7 +54400,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
53511
54400
  const ranked = /* @__PURE__ */ new Map();
53512
54401
  const managedCategories = /* @__PURE__ */ new Set();
53513
54402
  for (const [category, rules] of Object.entries(permission)) {
53514
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$9);
54403
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$10);
53515
54404
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
53516
54405
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
53517
54406
  for (const [pattern, action] of Object.entries(rules)) {
@@ -53782,7 +54671,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
53782
54671
  //#endregion
53783
54672
  //#region src/features/permissions/continue-permissions.ts
53784
54673
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
53785
- const CATCH_ALL_PATTERN$8 = "*";
54674
+ const CATCH_ALL_PATTERN$9 = "*";
53786
54675
  /**
53787
54676
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
53788
54677
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -53790,7 +54679,7 @@ const CATCH_ALL_PATTERN$8 = "*";
53790
54679
  * the bare entry instead of being skipped as a pattern-specific rule.
53791
54680
  */
53792
54681
  function normalizeCatchAllPattern$1(pattern) {
53793
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
54682
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
53794
54683
  }
53795
54684
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
53796
54685
  bash: "Bash",
@@ -53827,7 +54716,7 @@ function toCanonicalToolName$6(continueName) {
53827
54716
  * `Tool(pattern)`.
53828
54717
  */
53829
54718
  function buildContinuePermissionEntry(toolName, pattern) {
53830
- return pattern === CATCH_ALL_PATTERN$8 ? toolName : `${toolName}(${pattern})`;
54719
+ return pattern === CATCH_ALL_PATTERN$9 ? toolName : `${toolName}(${pattern})`;
53831
54720
  }
53832
54721
  /**
53833
54722
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -53841,7 +54730,7 @@ function parseContinuePermissionEntry(entry) {
53841
54730
  if (toolName === "") return;
53842
54731
  return {
53843
54732
  toolName,
53844
- pattern: pattern === "" ? CATCH_ALL_PATTERN$8 : pattern
54733
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$9 : pattern
53845
54734
  };
53846
54735
  }
53847
54736
  /**
@@ -54015,7 +54904,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
54015
54904
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
54016
54905
  continue;
54017
54906
  }
54018
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$8) {
54907
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$9) {
54019
54908
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
54020
54909
  continue;
54021
54910
  }
@@ -54364,14 +55253,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
54364
55253
  };
54365
55254
  //#endregion
54366
55255
  //#region src/features/permissions/crush-permissions.ts
54367
- const CATCH_ALL_PATTERN$7 = "*";
55256
+ const CATCH_ALL_PATTERN$8 = "*";
54368
55257
  /**
54369
55258
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
54370
55259
  * argument patterns at all, so it is the only pattern a rule can carry and
54371
55260
  * still be written.
54372
55261
  */
54373
55262
  function normalizeCatchAllPattern(pattern) {
54374
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$7 : pattern;
55263
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
54375
55264
  }
54376
55265
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
54377
55266
  bash: "bash",
@@ -54570,7 +55459,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
54570
55459
  for (const [rawPattern, action] of Object.entries(rules)) {
54571
55460
  if (isPrototypePollutionKey(rawPattern)) continue;
54572
55461
  const pattern = normalizeCatchAllPattern(rawPattern);
54573
- if (pattern === CATCH_ALL_PATTERN$7) {
55462
+ if (pattern === CATCH_ALL_PATTERN$8) {
54574
55463
  catchAllAction = action;
54575
55464
  continue;
54576
55465
  }
@@ -54643,11 +55532,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
54643
55532
  };
54644
55533
  for (const entry of disabled) {
54645
55534
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
54646
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$7] = "deny";
55535
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] = "deny";
54647
55536
  }
54648
55537
  for (const entry of allowed) {
54649
55538
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
54650
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$7] ??= "allow";
55539
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] ??= "allow";
54651
55540
  }
54652
55541
  return { permission };
54653
55542
  }
@@ -56512,7 +57401,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
56512
57401
  //#region src/features/permissions/goose-permissions.ts
56513
57402
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
56514
57403
  const GOOSE_USER_KEY = "user";
56515
- const CATCH_ALL_PATTERN$6 = "*";
57404
+ const CATCH_ALL_PATTERN$7 = "*";
56516
57405
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
56517
57406
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
56518
57407
  shell: ["bash"],
@@ -56676,7 +57565,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
56676
57565
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
56677
57566
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
56678
57567
  for (const [pattern, action] of Object.entries(rules)) {
56679
- if (pattern !== CATCH_ALL_PATTERN$6) {
57568
+ if (pattern !== CATCH_ALL_PATTERN$7) {
56680
57569
  logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
56681
57570
  continue;
56682
57571
  }
@@ -56707,7 +57596,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
56707
57596
  const action = GOOSE_LIST_TO_ACTION[key];
56708
57597
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
56709
57598
  permission[category] ??= {};
56710
- permission[category][CATCH_ALL_PATTERN$6] = action;
57599
+ permission[category][CATCH_ALL_PATTERN$7] = action;
56711
57600
  }
56712
57601
  }
56713
57602
  return { permission };
@@ -56718,7 +57607,7 @@ const GROKCLI_UI_KEY = "ui";
56718
57607
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
56719
57608
  const GROKCLI_PERMISSION_KEY = "permission";
56720
57609
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
56721
- const CATCH_ALL_PATTERN$5 = "*";
57610
+ const CATCH_ALL_PATTERN$6 = "*";
56722
57611
  const MCP_CANONICAL_PREFIX$3 = "mcp__";
56723
57612
  const CATEGORY_TO_GROK_TOOL = {
56724
57613
  bash: "Bash",
@@ -56764,10 +57653,10 @@ function buildGrokEntry(category, pattern) {
56764
57653
  const remainder = category.slice(5);
56765
57654
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
56766
57655
  }
56767
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$5 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
57656
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
56768
57657
  const tool = CATEGORY_TO_GROK_TOOL[category];
56769
57658
  if (tool === void 0) return null;
56770
- return pattern === CATCH_ALL_PATTERN$5 || pattern === "" ? tool : `${tool}(${pattern})`;
57659
+ return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? tool : `${tool}(${pattern})`;
56771
57660
  }
56772
57661
  /**
56773
57662
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -56788,16 +57677,16 @@ function parseGrokEntry(entry) {
56788
57677
  }
56789
57678
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
56790
57679
  category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
56791
- pattern: CATCH_ALL_PATTERN$5
57680
+ pattern: CATCH_ALL_PATTERN$6
56792
57681
  } : {
56793
57682
  category: "mcp",
56794
- pattern: CATCH_ALL_PATTERN$5
57683
+ pattern: CATCH_ALL_PATTERN$6
56795
57684
  };
56796
57685
  const category = GROK_TOOL_TO_CATEGORY[tool];
56797
57686
  if (category === void 0) return null;
56798
57687
  return {
56799
57688
  category,
56800
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$5
57689
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$6
56801
57690
  };
56802
57691
  }
56803
57692
  /**
@@ -56818,15 +57707,15 @@ function parseGrokRule(rule) {
56818
57707
  if (typeof tool !== "string") return null;
56819
57708
  if (pattern !== void 0 && typeof pattern !== "string") return null;
56820
57709
  const trimmedPattern = pattern?.trim() ?? "";
56821
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$5;
57710
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$6;
56822
57711
  const lowerTool = tool.trim().toLowerCase();
56823
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$5 ? {
57712
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$6 ? {
56824
57713
  category: "mcp",
56825
- pattern: CATCH_ALL_PATTERN$5,
57714
+ pattern: CATCH_ALL_PATTERN$6,
56826
57715
  action
56827
57716
  } : {
56828
57717
  category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
56829
- pattern: CATCH_ALL_PATTERN$5,
57718
+ pattern: CATCH_ALL_PATTERN$6,
56830
57719
  action
56831
57720
  };
56832
57721
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -56974,7 +57863,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
56974
57863
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
56975
57864
  }
56976
57865
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
56977
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$5]: legacyModeAction(parsed) } } };
57866
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$6]: legacyModeAction(parsed) } } };
56978
57867
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
56979
57868
  }
56980
57869
  validate() {
@@ -57160,12 +58049,12 @@ function reconcileNativeDenies({ permission, category, patterns }) {
57160
58049
  deleteRulesByAction(rules, "deny");
57161
58050
  for (const pattern of patterns) rules[pattern] = "deny";
57162
58051
  }
57163
- function withoutKey(record, key) {
58052
+ function withoutKey$1(record, key) {
57164
58053
  return Object.fromEntries(Object.entries(record).filter(([entryKey]) => entryKey !== key));
57165
58054
  }
57166
58055
  function buildHermesOverride(config, provenance) {
57167
58056
  const base = isRecord$1(provenance.hermes) ? { ...provenance.hermes } : {};
57168
- const approvalsOverride = withoutKey(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
58057
+ const approvalsOverride = withoutKey$1(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
57169
58058
  if (Object.keys(approvalsOverride).length > 0) base.approvals = approvalsOverride;
57170
58059
  else delete base.approvals;
57171
58060
  const security = isRecord$1(config.security) ? { ...config.security } : {};
@@ -57750,6 +58639,35 @@ function narrowSandboxToProjectScope({ authored, logger }) {
57750
58639
  if (dropped.length > 0) logger?.warn(`Kilo honors these 'sandbox' keys from the global config only, so they were dropped from the project config: ${dropped.toSorted().join(", ")}. A project config may only tighten the sandbox ('enabled', 'network'); generate with --global to author the rest.`);
57751
58640
  return emitted;
57752
58641
  }
58642
+ const KILO_MARKDOWN_SOURCE_KEY = "markdown_source";
58643
+ /**
58644
+ * Narrow an authored `markdown_source` rule to what a *project* `kilo.jsonc`
58645
+ * can mean. Kilo only grants it when the winning pattern came from the global
58646
+ * config with the action `allow` ("Project configuration cannot grant this
58647
+ * permission"). A project pattern that wins last-match-wins therefore never
58648
+ * grants: `deny` and `ask` both block (Kilo never prompts for it), so they
58649
+ * are kept — a project config may only tighten. A project `allow` would block
58650
+ * too, the opposite of what it says, so it is dropped with a warning pointing
58651
+ * at `--global`.
58652
+ * Returns `undefined` when nothing is left to write.
58653
+ * @see https://kilo.ai/docs/customize/workflows
58654
+ * @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/config/external-markdown.ts
58655
+ */
58656
+ function narrowMarkdownSourceToProjectScope({ authored, logger }) {
58657
+ let emitted;
58658
+ let dropped;
58659
+ if (typeof authored === "string") {
58660
+ emitted = authored === "allow" ? void 0 : authored;
58661
+ dropped = authored === "allow" ? ["*"] : [];
58662
+ } else {
58663
+ const entries = Object.entries(asKiloRecord(authored));
58664
+ const kept = entries.filter(([, action]) => action !== "allow");
58665
+ emitted = kept.length > 0 ? Object.fromEntries(kept) : void 0;
58666
+ dropped = entries.filter(([, action]) => action === "allow").map(([pattern]) => pattern);
58667
+ }
58668
+ if (dropped.length > 0) logger?.warn(`Kilo grants '${KILO_MARKDOWN_SOURCE_KEY}' from the global config only, so these 'allow' patterns were dropped from the project config: ${dropped.join(", ")}. A project config can only block it ('deny' or 'ask'); generate with --global to grant access.`);
58669
+ return emitted;
58670
+ }
57753
58671
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
57754
58672
  json;
57755
58673
  constructor(params) {
@@ -57836,6 +58754,13 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
57836
58754
  ...honorAllToolsOnBash(rulesyncJson.permission),
57837
58755
  ...kiloOverride?.permission
57838
58756
  };
58757
+ if (!global && Object.hasOwn(incomingPermission, KILO_MARKDOWN_SOURCE_KEY)) {
58758
+ const narrowed = narrowMarkdownSourceToProjectScope({
58759
+ authored: incomingPermission[KILO_MARKDOWN_SOURCE_KEY],
58760
+ logger
58761
+ });
58762
+ incomingPermission[KILO_MARKDOWN_SOURCE_KEY] = narrowed;
58763
+ }
57839
58764
  const droppedDenyByKey = {};
57840
58765
  for (const [key, value] of Object.entries(incomingPermission)) {
57841
58766
  const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
@@ -57851,6 +58776,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
57851
58776
  ...existingPermission,
57852
58777
  ...incomingPermission
57853
58778
  };
58779
+ if (mergedPermission[KILO_MARKDOWN_SOURCE_KEY] === void 0) delete mergedPermission[KILO_MARKDOWN_SOURCE_KEY];
57854
58780
  const nextJson = {
57855
58781
  ...parsed,
57856
58782
  permission: mergedPermission
@@ -58527,7 +59453,7 @@ function rulesFromArrays(settings, allowKey, denyKey) {
58527
59453
  //#region src/features/permissions/lettacode-permissions.ts
58528
59454
  /** Top-level key of `.letta/settings.json` that rulesync owns. */
58529
59455
  const LETTACODE_PERMISSIONS_KEY = "permissions";
58530
- const CATCH_ALL_PATTERN$4 = "*";
59456
+ const CATCH_ALL_PATTERN$5 = "*";
58531
59457
  /** Letta Code's `Bash(prefix:*)` prefix-match suffix. */
58532
59458
  const PREFIX_SUFFIX = ":*";
58533
59459
  const CATEGORY_TO_LETTACODE_TOOL = {
@@ -58620,7 +59546,7 @@ function categoryOfRule$1(rule) {
58620
59546
  * widening it would grant commands the source never allowed.
58621
59547
  */
58622
59548
  function toLettacodeBashPayload({ pattern, action, logger }) {
58623
- if (pattern === CATCH_ALL_PATTERN$4) return "";
59549
+ if (pattern === CATCH_ALL_PATTERN$5) return "";
58624
59550
  const firstStar = pattern.indexOf("*");
58625
59551
  if (firstStar === -1) return pattern;
58626
59552
  if (pattern.endsWith(PREFIX_SUFFIX) && firstStar === pattern.length - 1) return pattern;
@@ -58641,7 +59567,7 @@ function toLettacodeRule({ category, pattern, action, logger }) {
58641
59567
  pattern,
58642
59568
  action,
58643
59569
  logger
58644
- }) : pattern === CATCH_ALL_PATTERN$4 ? "" : pattern;
59570
+ }) : pattern === CATCH_ALL_PATTERN$5 ? "" : pattern;
58645
59571
  if (payload === null) return null;
58646
59572
  return payload === "" ? tool : `${tool}(${payload})`;
58647
59573
  }
@@ -58652,7 +59578,7 @@ function fromLettacodeRule(rule) {
58652
59578
  const { payload } = splitLettacodeRule(rule);
58653
59579
  if (payload === void 0) return {
58654
59580
  category,
58655
- pattern: CATCH_ALL_PATTERN$4
59581
+ pattern: CATCH_ALL_PATTERN$5
58656
59582
  };
58657
59583
  const trimmed = payload.trim();
58658
59584
  if (trimmed === "") return null;
@@ -58660,7 +59586,7 @@ function fromLettacodeRule(rule) {
58660
59586
  const prefix = trimmed.slice(0, -2).trimEnd();
58661
59587
  return {
58662
59588
  category,
58663
- pattern: prefix === "" ? CATCH_ALL_PATTERN$4 : `${prefix} *`
59589
+ pattern: prefix === "" ? CATCH_ALL_PATTERN$5 : `${prefix} *`
58664
59590
  };
58665
59591
  }
58666
59592
  return {
@@ -59116,6 +60042,377 @@ var MimocodePermissions = class extends OpencodePermissions {
59116
60042
  static layout = MIMOCODE_LAYOUT;
59117
60043
  };
59118
60044
  //#endregion
60045
+ //#region src/features/permissions/omp-permissions.ts
60046
+ const CATCH_ALL_PATTERN$4 = "*";
60047
+ const ACTION_TO_OMP_APPROVAL = {
60048
+ allow: "allow",
60049
+ ask: "prompt",
60050
+ deny: "deny"
60051
+ };
60052
+ const OMP_APPROVAL_TO_ACTION = {
60053
+ allow: "allow",
60054
+ prompt: "ask",
60055
+ deny: "deny"
60056
+ };
60057
+ const CATEGORY_TO_OMP_TOOL = {
60058
+ websearch: "web_search",
60059
+ agent: "task"
60060
+ };
60061
+ const OMP_TOOL_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_TO_OMP_TOOL).map(([category, tool]) => [tool, category]));
60062
+ const UNSUPPORTED_CATEGORIES = /* @__PURE__ */ new Set([
60063
+ "webfetch",
60064
+ "notebookedit",
60065
+ "mcp"
60066
+ ]);
60067
+ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
60068
+ "bash",
60069
+ "read",
60070
+ "edit",
60071
+ "write",
60072
+ "grep",
60073
+ "glob",
60074
+ "web_search",
60075
+ "task"
60076
+ ]);
60077
+ /**
60078
+ * Permissions adapter for oh-my-pi (`omp`).
60079
+ *
60080
+ * oh-my-pi reads approval policy from `config.yml`: `<cwd>/.omp/config.yml` in
60081
+ * project scope and `~/.omp/agent/config.yml` (default profile) globally.
60082
+ *
60083
+ * - `tools.approval.<tool>: allow | prompt | deny` is a per-tool policy honored
60084
+ * in every approval mode. A category's catch-all `*` rule maps here
60085
+ * (`ask` → `prompt`). oh-my-pi has no per-path matcher, so a non-bash category
60086
+ * with pattern-specific rules collapses to one action (deny > ask > allow, with
60087
+ * an implicit `ask` when there is no catch-all) and a warning.
60088
+ * - `bash.patterns: [{ match, approval }]` is an ordered, first-match-wins list
60089
+ * of command globs (only `*` is a wildcard). Every `bash` rule except a
60090
+ * catch-all `allow` is written there, ordered deny → prompt → allow so that
60091
+ * the first match agrees with rulesync's deny > ask > allow precedence. A
60092
+ * catch-all `allow` goes to `tools.approval.bash` instead, because by default
60093
+ * (without `bash.allowCompoundCommands`) an `allow` pattern never approves a
60094
+ * compound command line while the tool policy does — unless the category has
60095
+ * an `ask` rule, which makes `tools.approval.bash` `prompt` (see
60096
+ * `convertBashRules`) and the catch-all `allow` a trailing pattern. oh-my-pi
60097
+ * expands no `?` or `[...]`, so a deny or ask pattern widens them to `*`.
60098
+ * - The all-tools `*` category has no key of its own: its rules restrict bash
60099
+ * commands, and its deny / ask also restrict every other tool's policy (a
60100
+ * catch-all one at its own strictness, a pattern-specific one as `prompt`).
60101
+ *
60102
+ * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
60103
+ * (the keys rulesync manages) and `bash.patterns` are rewritten; sibling keys
60104
+ * such as `tools.approvalMode` and `bash.enabled` are kept, and the file is
60105
+ * never deleted.
60106
+ *
60107
+ * @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
60108
+ * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
60109
+ */
60110
+ var OmpPermissions = class OmpPermissions extends ToolPermissions {
60111
+ constructor(params) {
60112
+ super({
60113
+ ...params,
60114
+ fileContent: params.fileContent ?? ""
60115
+ });
60116
+ }
60117
+ /** `config.yml` holds unrelated user settings, so it must not be deleted. */
60118
+ isDeletable() {
60119
+ return false;
60120
+ }
60121
+ static getSettablePaths({ global = false } = {}) {
60122
+ return {
60123
+ relativeDirPath: global ? OMP_GLOBAL_DIR : OMP_DIR,
60124
+ relativeFilePath: OMP_CONFIG_FILE_NAME
60125
+ };
60126
+ }
60127
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
60128
+ const paths = OmpPermissions.getSettablePaths({ global });
60129
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
60130
+ return new OmpPermissions({
60131
+ outputRoot,
60132
+ relativeDirPath: paths.relativeDirPath,
60133
+ relativeFilePath: paths.relativeFilePath,
60134
+ fileContent,
60135
+ validate,
60136
+ global
60137
+ });
60138
+ }
60139
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
60140
+ const paths = OmpPermissions.getSettablePaths({ global });
60141
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
60142
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
60143
+ const existing = parseSharedConfig({
60144
+ format: "yaml",
60145
+ fileContent: existingContent,
60146
+ filePath
60147
+ });
60148
+ const { approval, patterns, floor } = convertRulesyncToOmp({
60149
+ config: rulesyncPermissions.getJson(),
60150
+ logger
60151
+ });
60152
+ const existingTools = isRecord$1(existing.tools) ? existing.tools : {};
60153
+ const nextApproval = {
60154
+ ...isRecord$1(existingTools.approval) ? Object.fromEntries(Object.entries(existingTools.approval).filter(([tool]) => !MANAGED_OMP_TOOLS.has(tool) && !Object.hasOwn(approval, tool)).map(([tool, value]) => [tool, raisedApproval(value, floor)])) : {},
60155
+ ...approval
60156
+ };
60157
+ const toolsSiblings = withoutKey(existingTools, "approval");
60158
+ const nextTools = Object.keys(nextApproval).length > 0 ? {
60159
+ ...toolsSiblings,
60160
+ approval: nextApproval
60161
+ } : toolsSiblings;
60162
+ const bashSiblings = withoutKey(isRecord$1(existing.bash) ? existing.bash : {}, "patterns");
60163
+ const nextBash = patterns.length > 0 ? {
60164
+ ...bashSiblings,
60165
+ patterns
60166
+ } : bashSiblings;
60167
+ return new OmpPermissions({
60168
+ outputRoot,
60169
+ relativeDirPath: paths.relativeDirPath,
60170
+ relativeFilePath: paths.relativeFilePath,
60171
+ fileContent: applySharedConfigPatch({
60172
+ fileKey: sharedConfigFileKey(paths),
60173
+ feature: "permissions",
60174
+ existingContent,
60175
+ patch: {
60176
+ tools: Object.keys(nextTools).length > 0 ? nextTools : void 0,
60177
+ bash: Object.keys(nextBash).length > 0 ? nextBash : void 0
60178
+ },
60179
+ filePath,
60180
+ logger
60181
+ }),
60182
+ validate: true,
60183
+ global
60184
+ });
60185
+ }
60186
+ toRulesyncPermissions() {
60187
+ let config;
60188
+ try {
60189
+ config = parseSharedConfig({
60190
+ format: "yaml",
60191
+ fileContent: this.getFileContent()
60192
+ });
60193
+ } catch (error) {
60194
+ throw new Error(`Failed to parse oh-my-pi config in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
60195
+ }
60196
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(convertOmpToRulesync(config), null, 2) });
60197
+ }
60198
+ validate() {
60199
+ return {
60200
+ success: true,
60201
+ error: null
60202
+ };
60203
+ }
60204
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
60205
+ return new OmpPermissions({
60206
+ outputRoot,
60207
+ relativeDirPath,
60208
+ relativeFilePath,
60209
+ fileContent: "",
60210
+ validate: false,
60211
+ global
60212
+ });
60213
+ }
60214
+ };
60215
+ function withoutKey(record, key) {
60216
+ return Object.fromEntries(Object.entries(record).filter(([name]) => name !== key));
60217
+ }
60218
+ function renamed(map, name) {
60219
+ return Object.hasOwn(map, name) ? map[name] ?? name : name;
60220
+ }
60221
+ function stricter(a, b) {
60222
+ return a !== void 0 && PERMISSION_ACTION_PRIORITY[a] >= PERMISSION_ACTION_PRIORITY[b] ? a : b;
60223
+ }
60224
+ /**
60225
+ * The floor the all-tools `*` category puts under every tool's single policy:
60226
+ * a catch-all deny / ask at its own strictness, and a pattern-specific one as
60227
+ * `ask`, since some input of each tool may match it and a per-tool policy
60228
+ * cannot single that input out. `undefined` when `*` restricts nothing.
60229
+ */
60230
+ function allToolsFloor(rules) {
60231
+ let floor;
60232
+ let patternSpecific = false;
60233
+ for (const [pattern, action] of Object.entries(rules ?? {})) {
60234
+ if (action === "allow" || isPrototypePollutionKey(pattern)) continue;
60235
+ if (pattern === CATCH_ALL_PATTERN$4) floor = stricter(floor, action);
60236
+ else {
60237
+ patternSpecific = true;
60238
+ floor = stricter(floor, "ask");
60239
+ }
60240
+ }
60241
+ return {
60242
+ floor,
60243
+ patternSpecific
60244
+ };
60245
+ }
60246
+ /**
60247
+ * oh-my-pi expands only `*`, so a `?` or `[...]` in a deny / ask pattern widens
60248
+ * to `*` (a literal match would restrict almost nothing). An allow pattern is
60249
+ * left as is: matched literally it approves less, never more.
60250
+ */
60251
+ function widenRestrictionPattern(pattern) {
60252
+ return pattern.replace(/\[!?\]?[^\]]*\]|\[|\?/gu, "*").replace(/\*{2,}/gu, "*");
60253
+ }
60254
+ /**
60255
+ * A `bash` category as `bash.patterns` entries (unsorted), plus the
60256
+ * `tools.approval.bash` policy it needs.
60257
+ *
60258
+ * oh-my-pi replaces a `prompt` (or `allow`) pattern match on a critical command
60259
+ * such as `rm -rf /` with an override that carries no policy, which yolo mode
60260
+ * then resolves by `tools.approval.bash` alone. So any `ask` rule sets the tool
60261
+ * policy to `prompt`, and a catch-all `allow` then becomes a trailing
60262
+ * `match: "*"` allow pattern (patterns beat the tool policy) instead of an
60263
+ * `allow` policy that would wave such a command through.
60264
+ */
60265
+ function convertBashRules$1({ rules, logger }) {
60266
+ const patterns = [];
60267
+ const entries = Object.entries(rules).filter(([pattern]) => !isPrototypePollutionKey(pattern));
60268
+ const hasAsk = entries.some(([, action]) => action === "ask");
60269
+ let policy = hasAsk ? "ask" : void 0;
60270
+ if (hasAsk && rules[CATCH_ALL_PATTERN$4] !== "ask") logger?.warn("oh-my-pi lets a critical command (such as `rm -rf /`) skip a bash prompt pattern unless the bash tool policy prompts, so `tools.approval.bash` was set to \"prompt\": a command no bash allow pattern matches now prompts in every approval mode.");
60271
+ for (const [pattern, action] of entries) {
60272
+ if (pattern === CATCH_ALL_PATTERN$4 && action === "allow") {
60273
+ if (hasAsk) patterns.push({
60274
+ match: CATCH_ALL_PATTERN$4,
60275
+ approval: "allow"
60276
+ });
60277
+ else policy = "allow";
60278
+ continue;
60279
+ }
60280
+ let match = pattern;
60281
+ if (/[?[]/u.test(pattern)) {
60282
+ if (action === "allow") logger?.warn(`oh-my-pi matches only "*" as a wildcard, so "?" and "[" in the bash allow pattern "${pattern}" are matched literally.`);
60283
+ else {
60284
+ match = widenRestrictionPattern(pattern);
60285
+ logger?.warn(`oh-my-pi matches only "*" as a wildcard, so the bash ${action} pattern "${pattern}" was widened to "${match}".`);
60286
+ }
60287
+ }
60288
+ patterns.push({
60289
+ match,
60290
+ approval: ACTION_TO_OMP_APPROVAL[action]
60291
+ });
60292
+ }
60293
+ return {
60294
+ patterns,
60295
+ policy
60296
+ };
60297
+ }
60298
+ /** Raise every non-bash tool policy to the all-tools floor (bash is handled by patterns). */
60299
+ function applyAllToolsFloor({ actions, allTools, logger }) {
60300
+ const { floor } = allTools;
60301
+ if (floor === void 0) return;
60302
+ if (allTools.patternSpecific) logger?.warn(`oh-my-pi sets one approval policy per tool, so the pattern-specific "*" category restrictions were written as "${ACTION_TO_OMP_APPROVAL[floor]}" for every non-bash tool.`);
60303
+ for (const tool of /* @__PURE__ */ new Set([...MANAGED_OMP_TOOLS, ...Object.keys(actions)])) if (tool !== "bash") actions[tool] = stricter(actions[tool], floor);
60304
+ }
60305
+ function convertRulesyncToOmp({ config, logger }) {
60306
+ const actions = {};
60307
+ const patterns = [];
60308
+ const permission = config.permission;
60309
+ const allTools = allToolsFloor(permission["*"]);
60310
+ const categories = permission["*"] === void 0 ? permission : {
60311
+ ...permission,
60312
+ bash: bashRulesHonoringAllTools(permission)
60313
+ };
60314
+ for (const [category, rules] of Object.entries(categories)) {
60315
+ if (isPrototypePollutionKey(category) || Object.keys(rules).length === 0) continue;
60316
+ if (category === "*") {
60317
+ if (Object.values(rules).includes("allow")) logger?.warn("oh-my-pi has no all-tools approval key, so the \"*\" category allow rules were skipped.");
60318
+ continue;
60319
+ }
60320
+ if (UNSUPPORTED_CATEGORIES.has(category)) {
60321
+ logger?.warn(`oh-my-pi has no "${category}" tool to set an approval policy on, so its rules were skipped` + (category === "webfetch" ? " (oh-my-pi fetches web pages through `read`)." : "."));
60322
+ continue;
60323
+ }
60324
+ if (category === "bash") {
60325
+ const bash = convertBashRules$1({
60326
+ rules,
60327
+ logger
60328
+ });
60329
+ patterns.push(...bash.patterns);
60330
+ if (bash.policy !== void 0) actions.bash = bash.policy;
60331
+ continue;
60332
+ }
60333
+ const action = collapseRulesToSingleAction({ rules });
60334
+ if (action === void 0) continue;
60335
+ if (hasPatternSpecificRules(rules)) logger?.warn(`oh-my-pi sets one approval policy per tool, so the pattern-specific "${category}" rules were collapsed to "${ACTION_TO_OMP_APPROVAL[action]}" (deny > ask > allow).`);
60336
+ const tool = renamed(CATEGORY_TO_OMP_TOOL, category);
60337
+ if (/^mcp__.+__/u.test(tool)) logger?.warn(`oh-my-pi registers MCP tools as "mcp__<server>_<tool>", so "${tool}" may never match; write oh-my-pi's registered name under a tool-scoped \`omp.permission\` block.`);
60338
+ actions[tool] = stricter(actions[tool], action);
60339
+ }
60340
+ applyAllToolsFloor({
60341
+ actions,
60342
+ allTools,
60343
+ logger
60344
+ });
60345
+ const order = (pattern) => -PERMISSION_ACTION_PRIORITY[OMP_APPROVAL_TO_ACTION[pattern.approval]];
60346
+ patterns.sort((a, b) => order(a) - order(b));
60347
+ return {
60348
+ approval: Object.fromEntries(Object.entries(actions).map(([tool, action]) => [tool, ACTION_TO_OMP_APPROVAL[action]])),
60349
+ patterns,
60350
+ floor: allTools.floor
60351
+ };
60352
+ }
60353
+ /** An existing `tools.approval` value raised to the all-tools floor, if any. */
60354
+ function raisedApproval(value, floor) {
60355
+ if (floor === void 0) return value;
60356
+ const approval = toOmpApproval(value);
60357
+ const current = approval === void 0 ? void 0 : OMP_APPROVAL_TO_ACTION[approval];
60358
+ return ACTION_TO_OMP_APPROVAL[stricter(current, floor)];
60359
+ }
60360
+ function toOmpApproval(value) {
60361
+ if (typeof value !== "string") return;
60362
+ const normalized = value.trim().toLowerCase();
60363
+ return Object.hasOwn(OMP_APPROVAL_TO_ACTION, normalized) ? normalized : void 0;
60364
+ }
60365
+ /**
60366
+ * Fold `tools.approval.bash` (`undefined` when unset) into the imported `bash`
60367
+ * rules. A user `deny` is absolute. Otherwise a `*` pattern beats the tool
60368
+ * policy, which then reaches only compound and critical command lines — so a
60369
+ * `*` allow pattern imports as `allow` only behind an explicit `allow` policy
60370
+ * or in the shape rulesync writes (a `prompt` policy next to `ask` patterns).
60371
+ * Any other `*` allow pattern still prompts for compound lines and imports as
60372
+ * `ask`, so a regenerate cannot widen it into an `allow` policy. A `prompt`
60373
+ * policy with no `*` pattern but an `ask` pattern is what rulesync writes for
60374
+ * ask rules (see `convertBashRules`), so it adds no catch-all.
60375
+ */
60376
+ function importBashPolicy(bashRules, action) {
60377
+ const catchAll = bashRules[CATCH_ALL_PATTERN$4];
60378
+ const hasAskPattern = Object.values(bashRules).includes("ask");
60379
+ if (action === "deny") bashRules[CATCH_ALL_PATTERN$4] = "deny";
60380
+ else if (catchAll === void 0) {
60381
+ if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = action;
60382
+ } else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = "ask";
60383
+ }
60384
+ /** `bash.patterns` as canonical `bash` rules, in first-match order. */
60385
+ function importBashPatterns(value) {
60386
+ const bashRules = {};
60387
+ if (!Array.isArray(value)) return bashRules;
60388
+ for (const entry of value) {
60389
+ if (!isRecord$1(entry) || typeof entry.match !== "string") continue;
60390
+ const match = entry.match.trim().replace(/\s+/gu, " ");
60391
+ const approval = toOmpApproval(entry.approval);
60392
+ if (match === "" || approval === void 0 || Object.hasOwn(bashRules, match)) continue;
60393
+ if (isPrototypePollutionKey(match)) continue;
60394
+ bashRules[match] = OMP_APPROVAL_TO_ACTION[approval];
60395
+ }
60396
+ return bashRules;
60397
+ }
60398
+ function convertOmpToRulesync(config) {
60399
+ const permission = {};
60400
+ const tools = isRecord$1(config.tools) ? config.tools : {};
60401
+ const bashRules = importBashPatterns((isRecord$1(config.bash) ? config.bash : {}).patterns);
60402
+ const bashApproval = toOmpApproval((isRecord$1(tools.approval) ? tools.approval : {}).bash);
60403
+ importBashPolicy(bashRules, bashApproval === void 0 ? void 0 : OMP_APPROVAL_TO_ACTION[bashApproval]);
60404
+ if (isRecord$1(tools.approval)) for (const [tool, value] of Object.entries(tools.approval)) {
60405
+ const approval = toOmpApproval(value);
60406
+ if (approval === void 0 || tool.trim() === "" || isPrototypePollutionKey(tool)) continue;
60407
+ const action = OMP_APPROVAL_TO_ACTION[approval];
60408
+ if (tool === "bash") continue;
60409
+ const category = renamed(OMP_TOOL_TO_CATEGORY, tool);
60410
+ permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
60411
+ }
60412
+ if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
60413
+ return { permission };
60414
+ }
60415
+ //#endregion
59119
60416
  //#region src/features/permissions/pi-permissions.ts
59120
60417
  /** The one `settings.json` key this feature owns. */
59121
60418
  const DEFAULT_TOOLS_KEY = "defaultTools";
@@ -60143,7 +61440,9 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
60143
61440
  "eager",
60144
61441
  "listDirectory",
60145
61442
  "todoWrite",
60146
- "workflowsEnabled"
61443
+ "workflowsEnabled",
61444
+ "workflowNameOnly",
61445
+ "executionSandbox"
60147
61446
  ];
60148
61447
  const QWEN_OVERRIDE_SECURITY_KEYS = [
60149
61448
  "folderTrust",
@@ -60212,6 +61511,16 @@ const QWEN_OVERRIDE_GROUPS = [{
60212
61511
  overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
60213
61512
  scopedKeys: {
60214
61513
  workflowsEnabled: { rule: "workspace-stripped" },
61514
+ executionSandbox: {
61515
+ rule: "workspace-stripped",
61516
+ grants: () => true,
61517
+ globalNote: "Qwen Code ignores this key in workspace settings so a repository cannot choose its own sandbox; in the global scope it sets the filesystem and network policy every tool command runs under for every project on this machine (a System value still replaces it, and Qwen Code needs a restart to pick it up)."
61518
+ },
61519
+ workflowNameOnly: {
61520
+ rule: "global-machine-wide",
61521
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code lets a workspace only turn this key on: \`true\` restricts the model to named workflows in this repository, while any other value cannot lift a restriction set in user or system settings.`,
61522
+ globalNote: "Qwen Code honors this key in the global scope for every project on this machine, and a repository can only add the restriction, never lift it, so this decides whether the model may run inline or path-based workflow scripts anywhere a workspace does not turn the restriction on itself."
61523
+ },
60215
61524
  approvalMode: {
60216
61525
  rule: "global-machine-wide",
60217
61526
  projectNote: autonomyProjectNote
@@ -66030,6 +67339,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
66030
67339
  supportsImport: true
66031
67340
  }
66032
67341
  }],
67342
+ ["codewhale", {
67343
+ class: CodewhalePermissions,
67344
+ meta: {
67345
+ supportsProject: false,
67346
+ supportsGlobal: true,
67347
+ supportsImport: true
67348
+ }
67349
+ }],
66033
67350
  ["codexcli", {
66034
67351
  class: CodexcliPermissions,
66035
67352
  meta: {
@@ -66198,6 +67515,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
66198
67515
  supportsImport: true
66199
67516
  }
66200
67517
  }],
67518
+ ["omp", {
67519
+ class: OmpPermissions,
67520
+ meta: {
67521
+ supportsProject: true,
67522
+ supportsGlobal: true,
67523
+ supportsImport: true
67524
+ }
67525
+ }],
66201
67526
  ["opencode", {
66202
67527
  class: OpencodePermissions,
66203
67528
  meta: {
@@ -67077,6 +68402,10 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
67077
68402
  continue;
67078
68403
  }
67079
68404
  try {
68405
+ await assertWritablePathInsideRoot({
68406
+ rootPath: this.outputRoot,
68407
+ targetPath: root
68408
+ });
67080
68409
  await assertWritablePathInsideRoot({
67081
68410
  rootPath: this.outputRoot,
67082
68411
  targetPath: dirPath
@@ -67108,7 +68437,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
67108
68437
  this.logger.warn(`Refusing to delete ${quoteForLog(filePath)}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
67109
68438
  continue;
67110
68439
  }
67111
- if (isClaimed(filePath)) continue;
68440
+ if (await isClaimed(filePath)) continue;
67112
68441
  orphanPaths.add(filePath);
67113
68442
  }
67114
68443
  }
@@ -75664,16 +76993,30 @@ var SkillsProcessor = class extends DirFeatureProcessor {
75664
76993
  * path and vetted as writable inside this run's output root. Shared by the
75665
76994
  * two halves of the orphan sweep so both look in exactly the same places,
75666
76995
  * under exactly the same guard.
76996
+ *
76997
+ * A root that fails the guard — a symbolic link, even one that stays inside
76998
+ * the output root, such as a dotfiles checkout linked from the home
76999
+ * directory — is left out with a warning rather than failing the run. The
77000
+ * write path already wrote through such a link, and the sweep runs after
77001
+ * those writes land, so throwing here would report a failure for a run whose
77002
+ * output is in place. Sweeping through the link is not an option either: it
77003
+ * can lead to a directory rulesync does not manage, and its entries would be
77004
+ * deleted as orphans.
75667
77005
  */
75668
77006
  async loadExistingSkillsRoots(paths) {
75669
77007
  const existingRoots = [];
75670
77008
  for (const root of toolSkillSearchRoots(paths)) {
75671
77009
  const skillsDirPath = join(this.outputRoot, root);
75672
77010
  if (!await directoryExists(skillsDirPath)) continue;
75673
- await assertWritablePathInsideRoot({
75674
- rootPath: this.outputRoot,
75675
- targetPath: skillsDirPath
75676
- });
77011
+ try {
77012
+ await assertWritablePathInsideRoot({
77013
+ rootPath: this.outputRoot,
77014
+ targetPath: skillsDirPath
77015
+ });
77016
+ } catch (error) {
77017
+ this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
77018
+ continue;
77019
+ }
75677
77020
  existingRoots.push({
75678
77021
  root,
75679
77022
  skillsDirPath
@@ -76780,6 +78123,288 @@ var AugmentcodePluginSubagent = class extends AugmentcodeSubagent {
76780
78123
  }
76781
78124
  };
76782
78125
  //#endregion
78126
+ //#region src/features/subagents/roo-subagent.ts
78127
+ /**
78128
+ * Default tool groups assigned to a generated mode when the rulesync source
78129
+ * does not specify a `roo.groups` override. Mirrors Roo's built-in code mode
78130
+ * (full read/edit/command/mcp access; browser is opt-in).
78131
+ */
78132
+ const DEFAULT_GROUPS = [
78133
+ "read",
78134
+ "edit",
78135
+ "command",
78136
+ "mcp"
78137
+ ];
78138
+ /**
78139
+ * One entry of a mode's `groups` array. Either a bare toolset name
78140
+ * (`"read"`, `"edit"`, `"command"`, `"mcp"`, `"browser"`) or a tuple pairing a
78141
+ * toolset (in practice `"edit"`) with a file restriction object.
78142
+ * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
78143
+ */
78144
+ const RooModeGroupSchema = z.union([z.string(), z.tuple([z.string(), z.looseObject({})])]);
78145
+ /**
78146
+ * A single custom mode inside the `customModes` array of `.roomodes`.
78147
+ * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
78148
+ */
78149
+ const RooModeSchema = z.looseObject({
78150
+ slug: z.string(),
78151
+ name: z.string(),
78152
+ description: z.optional(z.string()),
78153
+ roleDefinition: z.string(),
78154
+ whenToUse: z.optional(z.string()),
78155
+ groups: z.optional(z.array(RooModeGroupSchema)),
78156
+ customInstructions: z.optional(z.string())
78157
+ });
78158
+ const RooModesFileSchema = z.looseObject({ customModes: z.array(RooModeSchema) });
78159
+ /**
78160
+ * Sanitize a string into a valid Roo mode slug (`^[a-zA-Z0-9-]+$`): lowercase,
78161
+ * non-alphanumeric runs collapsed to a single hyphen, leading/trailing hyphens
78162
+ * trimmed. Falls back to `"mode"` if nothing usable remains.
78163
+ */
78164
+ function sanitizeRooSlug(raw) {
78165
+ const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
78166
+ return slug.length > 0 ? slug : "mode";
78167
+ }
78168
+ function stringifyRooModes(modes) {
78169
+ return dump({ customModes: modes }, {
78170
+ lineWidth: -1,
78171
+ noRefs: true
78172
+ });
78173
+ }
78174
+ /**
78175
+ * Roo Code custom-modes adapter.
78176
+ *
78177
+ * Roo reads project-level custom modes from a single aggregated `.roomodes`
78178
+ * file at the workspace root (`docs.roocode.com` 301-redirects to the docs
78179
+ * below). Each rulesync subagent becomes one entry in the `customModes` array.
78180
+ * Because the native format aggregates N subagents into ONE file, every
78181
+ * `RooSubagent` instance carries the FULL `customModes` array and targets the
78182
+ * same `.roomodes` path; generation builds the aggregate once via
78183
+ * {@link RooSubagent.fromRulesyncSubagents}.
78184
+ *
78185
+ * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
78186
+ */
78187
+ var RooSubagent = class extends ToolSubagent {
78188
+ /** Name of the aggregated custom-modes file inside `getSettablePaths().relativeDirPath`. */
78189
+ static modesFileName = ROO_MODES_FILE_NAME;
78190
+ /** Frontmatter section of a rulesync subagent that carries the mode fields. */
78191
+ static modeSectionKey = "roo";
78192
+ /** Tool groups a generated mode gets when its section sets no `groups`. */
78193
+ static defaultGroups = DEFAULT_GROUPS;
78194
+ /** Tool target written into the frontmatter of imported subagents. */
78195
+ static importTarget = "roo";
78196
+ modes;
78197
+ constructor({ modes, ...rest }) {
78198
+ if (rest.validate !== false) {
78199
+ const result = RooModesFileSchema.safeParse({ customModes: modes });
78200
+ if (!result.success) throw new Error(`Invalid custom modes in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
78201
+ }
78202
+ super({
78203
+ ...rest,
78204
+ fileContent: stringifyRooModes(modes)
78205
+ });
78206
+ this.modes = modes;
78207
+ }
78208
+ static getSettablePaths(_options = {}) {
78209
+ return { relativeDirPath: "." };
78210
+ }
78211
+ getModes() {
78212
+ return this.modes;
78213
+ }
78214
+ getBody() {
78215
+ return this.fileContent;
78216
+ }
78217
+ /**
78218
+ * Map a single rulesync subagent to a Roo custom mode. The body becomes
78219
+ * `roleDefinition`; the optional `roo:` frontmatter section (the
78220
+ * {@link RooSubagent.modeSectionKey} section) supplies
78221
+ * `groups`, `whenToUse`, `customInstructions`, an explicit `slug`, and may
78222
+ * override `roleDefinition`. rulesync `name`/`description` are preferred,
78223
+ * while tool-specific `roo:` values take precedence where defined.
78224
+ */
78225
+ static toRooMode(rulesyncSubagent) {
78226
+ const frontmatter = rulesyncSubagent.getFrontmatter();
78227
+ const rawSection = frontmatter[this.modeSectionKey] ?? {};
78228
+ const slug = typeof rawSection.slug === "string" && rawSection.slug.length > 0 ? sanitizeRooSlug(rawSection.slug) : sanitizeRooSlug(basename(rulesyncSubagent.getRelativeFilePath()).replace(/\.[^.]+$/, ""));
78229
+ const roleDefinition = typeof rawSection.roleDefinition === "string" ? rawSection.roleDefinition : rulesyncSubagent.getBody();
78230
+ const groups = Array.isArray(rawSection.groups) ? rawSection.groups : [...this.defaultGroups];
78231
+ const mode = {
78232
+ slug,
78233
+ name: frontmatter.name,
78234
+ roleDefinition,
78235
+ groups
78236
+ };
78237
+ if (frontmatter.description) mode.description = frontmatter.description;
78238
+ if (typeof rawSection.whenToUse === "string") mode.whenToUse = rawSection.whenToUse;
78239
+ if (typeof rawSection.customInstructions === "string") mode.customInstructions = rawSection.customInstructions;
78240
+ return mode;
78241
+ }
78242
+ /**
78243
+ * Aggregate every targeted rulesync subagent into a single `.roomodes` file.
78244
+ * Modes are de-duplicated by `slug` (last one wins) to keep the array unique.
78245
+ */
78246
+ static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true }) {
78247
+ const bySlug = /* @__PURE__ */ new Map();
78248
+ for (const rulesyncSubagent of rulesyncSubagents) {
78249
+ const mode = this.toRooMode(rulesyncSubagent);
78250
+ bySlug.set(mode.slug, mode);
78251
+ }
78252
+ return new this({
78253
+ outputRoot,
78254
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
78255
+ relativeFilePath: this.modesFileName,
78256
+ modes: [...bySlug.values()],
78257
+ validate
78258
+ });
78259
+ }
78260
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true }) {
78261
+ return this.fromRulesyncSubagents({
78262
+ outputRoot,
78263
+ rulesyncSubagents: [rulesyncSubagent],
78264
+ validate
78265
+ });
78266
+ }
78267
+ /**
78268
+ * Convert every custom mode in the aggregated `.roomodes` file back into an
78269
+ * individual rulesync subagent. The processor calls this in the import
78270
+ * direction (one `.roomodes` tool file fans out to N rulesync files).
78271
+ */
78272
+ toRulesyncSubagents() {
78273
+ const { modeSectionKey, importTarget } = this.constructor;
78274
+ return this.modes.map((mode) => {
78275
+ const { slug, name, description, roleDefinition, whenToUse, groups, customInstructions, ...rest } = mode;
78276
+ const modeSection = {
78277
+ ...rest,
78278
+ slug,
78279
+ ...groups ? { groups } : {},
78280
+ ...whenToUse !== void 0 ? { whenToUse } : {},
78281
+ ...customInstructions !== void 0 ? { customInstructions } : {}
78282
+ };
78283
+ return new RulesyncSubagent({
78284
+ outputRoot: ".",
78285
+ frontmatter: {
78286
+ targets: [importTarget],
78287
+ name,
78288
+ ...description !== void 0 ? { description } : {},
78289
+ [modeSectionKey]: modeSection
78290
+ },
78291
+ body: roleDefinition,
78292
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
78293
+ relativeFilePath: `${sanitizeRooSlug(slug)}.md`,
78294
+ validate: true
78295
+ });
78296
+ });
78297
+ }
78298
+ toRulesyncSubagent() {
78299
+ const first = this.toRulesyncSubagents()[0];
78300
+ if (!first) throw new Error(`No custom modes found in ${join(this.relativeDirPath, this.relativeFilePath)} to convert.`);
78301
+ return first;
78302
+ }
78303
+ validate() {
78304
+ const result = RooModesFileSchema.safeParse({ customModes: this.modes });
78305
+ if (result.success) return {
78306
+ success: true,
78307
+ error: null
78308
+ };
78309
+ return {
78310
+ success: false,
78311
+ error: /* @__PURE__ */ new Error(`Invalid custom modes in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
78312
+ };
78313
+ }
78314
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
78315
+ return this.isTargetedByRulesyncSubagentDefault({
78316
+ rulesyncSubagent,
78317
+ toolTarget: "roo"
78318
+ });
78319
+ }
78320
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
78321
+ const paths = this.getSettablePaths();
78322
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
78323
+ const fileContent = await readFileContent(filePath);
78324
+ let parsed;
78325
+ try {
78326
+ parsed = loadYaml(fileContent);
78327
+ } catch (error) {
78328
+ throw new Error(`Failed to parse custom modes (${filePath}): ${formatError(error)}`, { cause: error });
78329
+ }
78330
+ const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
78331
+ if (!result.success) throw new Error(`Invalid custom modes in ${filePath}: ${formatError(result.error)}`);
78332
+ return new this({
78333
+ outputRoot,
78334
+ relativeDirPath: paths.relativeDirPath,
78335
+ relativeFilePath: basename(relativeFilePath),
78336
+ modes: result.data.customModes,
78337
+ validate
78338
+ });
78339
+ }
78340
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
78341
+ return new this({
78342
+ outputRoot,
78343
+ relativeDirPath,
78344
+ relativeFilePath,
78345
+ modes: [],
78346
+ validate: false
78347
+ });
78348
+ }
78349
+ };
78350
+ //#endregion
78351
+ //#region src/features/subagents/bob-subagent.ts
78352
+ /**
78353
+ * Subagent (custom-mode) generator for **IBM Bob** (Bob IDE and Bob Shell).
78354
+ *
78355
+ * Bob has no file-based subagents — its native subagents are the two fixed
78356
+ * presets `explore` and `general` — but it reads custom modes from an
78357
+ * aggregated `customModes` YAML file in Roo's format, so every targeted rulesync
78358
+ * subagent becomes one mode in `.bob/custom_modes.yaml`, exactly as the `roo`
78359
+ * target fills `.roomodes`. Only the project file is emitted: the Bob IDE and
78360
+ * Bob Shell docs name different global files.
78361
+ *
78362
+ * The mode fields ride a `bob:` frontmatter section rather than Roo's `roo:`
78363
+ * one, because Bob's tool groups are spelled differently (`execute` where Roo
78364
+ * says `command`, plus `skill`, `workflow`, `todo`, `subtask`, `subagent` and
78365
+ * `mode`), so a `roo.groups` list is not valid for Bob. A mode without
78366
+ * `bob.groups` gets `["read", "edit", "execute", "mcp"]`, Roo's default with
78367
+ * Bob IDE's spelling — the one Bob's shared approval settings use too. The Bob
78368
+ * Shell custom-modes page still lists Roo's `command` / `browser` groups, so a
78369
+ * mode meant for the Shell should spell its groups out in `bob.groups`.
78370
+ * `bob.allowedSubagents` (Bob IDE) is emitted when it is a string list, and
78371
+ * every other mode field is lifted back into `bob:` on import.
78372
+ *
78373
+ * @see https://bob.ibm.com/docs/ide/configuration/custom-modes
78374
+ * @see https://bob.ibm.com/docs/shell/configuration/custom-modes-bobshell
78375
+ * @see https://bob.ibm.com/docs/ide/features/subagents
78376
+ */
78377
+ var BobSubagent = class extends RooSubagent {
78378
+ static modesFileName = BOB_CUSTOM_MODES_FILE_NAME;
78379
+ static modeSectionKey = "bob";
78380
+ static defaultGroups = [
78381
+ "read",
78382
+ "edit",
78383
+ "execute",
78384
+ "mcp"
78385
+ ];
78386
+ static importTarget = "bob";
78387
+ static getSettablePaths(_options = {}) {
78388
+ return {
78389
+ relativeDirPath: BOB_DIR,
78390
+ relativeFilePath: BOB_CUSTOM_MODES_FILE_NAME
78391
+ };
78392
+ }
78393
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
78394
+ return this.isTargetedByRulesyncSubagentDefault({
78395
+ rulesyncSubagent,
78396
+ toolTarget: "bob"
78397
+ });
78398
+ }
78399
+ static toRooMode(rulesyncSubagent) {
78400
+ const mode = super.toRooMode(rulesyncSubagent);
78401
+ const frontmatter = rulesyncSubagent.getFrontmatter();
78402
+ const bobSection = isRecord$1(frontmatter.bob) ? frontmatter.bob : {};
78403
+ if (isStringArray$2(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
78404
+ return mode;
78405
+ }
78406
+ };
78407
+ //#endregion
76783
78408
  //#region src/features/subagents/claudecode-subagent.ts
76784
78409
  const ClaudecodeSubagentFrontmatterSchema = z.looseObject({
76785
78410
  name: z.string(),
@@ -78527,15 +80152,14 @@ var DevinSubagent = class extends ToolSubagent {
78527
80152
  toRulesyncSubagent() {
78528
80153
  const { name, description, ...restFields } = this.frontmatter;
78529
80154
  const devinSection = { ...restFields };
78530
- const rulesyncFrontmatter = {
78531
- targets: ["*"],
78532
- name,
78533
- description,
78534
- ...Object.keys(devinSection).length > 0 && { devin: devinSection }
78535
- };
78536
80155
  return new RulesyncSubagent({
78537
- outputRoot: this.getOutputRoot(),
78538
- frontmatter: rulesyncFrontmatter,
80156
+ outputRoot: ".",
80157
+ frontmatter: {
80158
+ targets: ["*"],
80159
+ name,
80160
+ description,
80161
+ ...Object.keys(devinSection).length > 0 && { devin: devinSection }
80162
+ },
78539
80163
  body: this.body,
78540
80164
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
78541
80165
  relativeFilePath: `${this.getSubagentName()}.md`,
@@ -81304,221 +82928,6 @@ var ReasonixSubagent = class ReasonixSubagent extends ToolSubagent {
81304
82928
  }
81305
82929
  };
81306
82930
  //#endregion
81307
- //#region src/features/subagents/roo-subagent.ts
81308
- /**
81309
- * Default tool groups assigned to a generated mode when the rulesync source
81310
- * does not specify a `roo.groups` override. Mirrors Roo's built-in code mode
81311
- * (full read/edit/command/mcp access; browser is opt-in).
81312
- */
81313
- const DEFAULT_GROUPS = [
81314
- "read",
81315
- "edit",
81316
- "command",
81317
- "mcp"
81318
- ];
81319
- /**
81320
- * One entry of a mode's `groups` array. Either a bare toolset name
81321
- * (`"read"`, `"edit"`, `"command"`, `"mcp"`, `"browser"`) or a tuple pairing a
81322
- * toolset (in practice `"edit"`) with a file restriction object.
81323
- * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
81324
- */
81325
- const RooModeGroupSchema = z.union([z.string(), z.tuple([z.string(), z.looseObject({})])]);
81326
- /**
81327
- * A single custom mode inside the `customModes` array of `.roomodes`.
81328
- * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
81329
- */
81330
- const RooModeSchema = z.looseObject({
81331
- slug: z.string(),
81332
- name: z.string(),
81333
- description: z.optional(z.string()),
81334
- roleDefinition: z.string(),
81335
- whenToUse: z.optional(z.string()),
81336
- groups: z.optional(z.array(RooModeGroupSchema)),
81337
- customInstructions: z.optional(z.string())
81338
- });
81339
- const RooModesFileSchema = z.looseObject({ customModes: z.array(RooModeSchema) });
81340
- /**
81341
- * Sanitize a string into a valid Roo mode slug (`^[a-zA-Z0-9-]+$`): lowercase,
81342
- * non-alphanumeric runs collapsed to a single hyphen, leading/trailing hyphens
81343
- * trimmed. Falls back to `"mode"` if nothing usable remains.
81344
- */
81345
- function sanitizeRooSlug(raw) {
81346
- const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
81347
- return slug.length > 0 ? slug : "mode";
81348
- }
81349
- function stringifyRooModes(modes) {
81350
- return dump({ customModes: modes }, {
81351
- lineWidth: -1,
81352
- noRefs: true
81353
- });
81354
- }
81355
- /**
81356
- * Roo Code custom-modes adapter.
81357
- *
81358
- * Roo reads project-level custom modes from a single aggregated `.roomodes`
81359
- * file at the workspace root (`docs.roocode.com` 301-redirects to the docs
81360
- * below). Each rulesync subagent becomes one entry in the `customModes` array.
81361
- * Because the native format aggregates N subagents into ONE file, every
81362
- * `RooSubagent` instance carries the FULL `customModes` array and targets the
81363
- * same `.roomodes` path; generation builds the aggregate once via
81364
- * {@link RooSubagent.fromRulesyncSubagents}.
81365
- *
81366
- * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
81367
- */
81368
- var RooSubagent = class extends ToolSubagent {
81369
- modes;
81370
- constructor({ modes, ...rest }) {
81371
- if (rest.validate !== false) {
81372
- const result = RooModesFileSchema.safeParse({ customModes: modes });
81373
- if (!result.success) throw new Error(`Invalid .roomodes in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
81374
- }
81375
- super({
81376
- ...rest,
81377
- fileContent: stringifyRooModes(modes)
81378
- });
81379
- this.modes = modes;
81380
- }
81381
- static getSettablePaths(_options = {}) {
81382
- return { relativeDirPath: "." };
81383
- }
81384
- getModes() {
81385
- return this.modes;
81386
- }
81387
- getBody() {
81388
- return this.fileContent;
81389
- }
81390
- /**
81391
- * Map a single rulesync subagent to a Roo custom mode. The body becomes
81392
- * `roleDefinition`; the optional `roo:` frontmatter section supplies
81393
- * `groups`, `whenToUse`, `customInstructions`, an explicit `slug`, and may
81394
- * override `roleDefinition`. rulesync `name`/`description` are preferred,
81395
- * while tool-specific `roo:` values take precedence where defined.
81396
- */
81397
- static toRooMode(rulesyncSubagent) {
81398
- const frontmatter = rulesyncSubagent.getFrontmatter();
81399
- const rawSection = frontmatter.roo ?? {};
81400
- const slug = typeof rawSection.slug === "string" && rawSection.slug.length > 0 ? sanitizeRooSlug(rawSection.slug) : sanitizeRooSlug(basename(rulesyncSubagent.getRelativeFilePath()).replace(/\.[^.]+$/, ""));
81401
- const roleDefinition = typeof rawSection.roleDefinition === "string" ? rawSection.roleDefinition : rulesyncSubagent.getBody();
81402
- const groups = Array.isArray(rawSection.groups) ? rawSection.groups : [...DEFAULT_GROUPS];
81403
- const mode = {
81404
- slug,
81405
- name: frontmatter.name,
81406
- roleDefinition,
81407
- groups
81408
- };
81409
- if (frontmatter.description) mode.description = frontmatter.description;
81410
- if (typeof rawSection.whenToUse === "string") mode.whenToUse = rawSection.whenToUse;
81411
- if (typeof rawSection.customInstructions === "string") mode.customInstructions = rawSection.customInstructions;
81412
- return mode;
81413
- }
81414
- /**
81415
- * Aggregate every targeted rulesync subagent into a single `.roomodes` file.
81416
- * Modes are de-duplicated by `slug` (last one wins) to keep the array unique.
81417
- */
81418
- static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true }) {
81419
- const bySlug = /* @__PURE__ */ new Map();
81420
- for (const rulesyncSubagent of rulesyncSubagents) {
81421
- const mode = this.toRooMode(rulesyncSubagent);
81422
- bySlug.set(mode.slug, mode);
81423
- }
81424
- return new this({
81425
- outputRoot,
81426
- relativeDirPath: this.getSettablePaths().relativeDirPath,
81427
- relativeFilePath: ROO_MODES_FILE_NAME,
81428
- modes: [...bySlug.values()],
81429
- validate
81430
- });
81431
- }
81432
- static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true }) {
81433
- return this.fromRulesyncSubagents({
81434
- outputRoot,
81435
- rulesyncSubagents: [rulesyncSubagent],
81436
- validate
81437
- });
81438
- }
81439
- /**
81440
- * Convert every custom mode in the aggregated `.roomodes` file back into an
81441
- * individual rulesync subagent. The processor calls this in the import
81442
- * direction (one `.roomodes` tool file fans out to N rulesync files).
81443
- */
81444
- toRulesyncSubagents() {
81445
- return this.modes.map((mode) => {
81446
- const { slug, name, description, roleDefinition, whenToUse, groups, customInstructions, ...rest } = mode;
81447
- const rooSection = {
81448
- ...rest,
81449
- slug,
81450
- ...groups ? { groups } : {},
81451
- ...whenToUse !== void 0 ? { whenToUse } : {},
81452
- ...customInstructions !== void 0 ? { customInstructions } : {}
81453
- };
81454
- return new RulesyncSubagent({
81455
- outputRoot: ".",
81456
- frontmatter: {
81457
- targets: ["roo"],
81458
- name,
81459
- ...description !== void 0 ? { description } : {},
81460
- roo: rooSection
81461
- },
81462
- body: roleDefinition,
81463
- relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
81464
- relativeFilePath: `${sanitizeRooSlug(slug)}.md`,
81465
- validate: true
81466
- });
81467
- });
81468
- }
81469
- toRulesyncSubagent() {
81470
- const first = this.toRulesyncSubagents()[0];
81471
- if (!first) throw new Error("No custom modes found in .roomodes to convert.");
81472
- return first;
81473
- }
81474
- validate() {
81475
- const result = RooModesFileSchema.safeParse({ customModes: this.modes });
81476
- if (result.success) return {
81477
- success: true,
81478
- error: null
81479
- };
81480
- return {
81481
- success: false,
81482
- error: /* @__PURE__ */ new Error(`Invalid .roomodes in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
81483
- };
81484
- }
81485
- static isTargetedByRulesyncSubagent(rulesyncSubagent) {
81486
- return this.isTargetedByRulesyncSubagentDefault({
81487
- rulesyncSubagent,
81488
- toolTarget: "roo"
81489
- });
81490
- }
81491
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
81492
- const paths = this.getSettablePaths();
81493
- const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
81494
- const fileContent = await readFileContent(filePath);
81495
- let parsed;
81496
- try {
81497
- parsed = loadYaml(fileContent);
81498
- } catch (error) {
81499
- throw new Error(`Failed to parse .roomodes (${filePath}): ${formatError(error)}`, { cause: error });
81500
- }
81501
- const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
81502
- if (!result.success) throw new Error(`Invalid .roomodes in ${filePath}: ${formatError(result.error)}`);
81503
- return new this({
81504
- outputRoot,
81505
- relativeDirPath: paths.relativeDirPath,
81506
- relativeFilePath: basename(relativeFilePath),
81507
- modes: result.data.customModes,
81508
- validate
81509
- });
81510
- }
81511
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
81512
- return new this({
81513
- outputRoot,
81514
- relativeDirPath,
81515
- relativeFilePath,
81516
- modes: [],
81517
- validate: false
81518
- });
81519
- }
81520
- };
81521
- //#endregion
81522
82931
  //#region src/features/subagents/tabnine-subagent.ts
81523
82932
  const TabnineSubagentFrontmatterSchema = z.looseObject({
81524
82933
  name: z.string(),
@@ -82016,20 +83425,19 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
82016
83425
  const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
82017
83426
  const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
82018
83427
  const fileStem = basename(this.getRelativeFilePath(), ".toml");
82019
- const rulesyncFrontmatter = {
82020
- targets: ["vibe"],
82021
- name: display_name ?? fileStem,
82022
- ...description !== void 0 && { description },
82023
- vibe: {
82024
- ...display_name !== void 0 && { display_name },
82025
- ...description !== void 0 && { description },
82026
- ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
82027
- ...vibeSection
82028
- }
82029
- };
82030
83428
  return new RulesyncSubagent({
82031
- outputRoot: this.outputRoot,
82032
- frontmatter: rulesyncFrontmatter,
83429
+ outputRoot: ".",
83430
+ frontmatter: {
83431
+ targets: ["vibe"],
83432
+ name: display_name ?? fileStem,
83433
+ ...description !== void 0 && { description },
83434
+ vibe: {
83435
+ ...display_name !== void 0 && { display_name },
83436
+ ...description !== void 0 && { description },
83437
+ ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
83438
+ ...vibeSection
83439
+ }
83440
+ },
82033
83441
  body: resolvedBody ?? "",
82034
83442
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
82035
83443
  relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
@@ -82462,6 +83870,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
82462
83870
  filePattern: "*.md"
82463
83871
  }
82464
83872
  }],
83873
+ ["bob", {
83874
+ class: BobSubagent,
83875
+ meta: {
83876
+ supportsProject: true,
83877
+ supportsSimulated: false,
83878
+ supportsGlobal: false,
83879
+ filePattern: "custom_modes.yaml"
83880
+ }
83881
+ }],
82465
83882
  ["claudecode", {
82466
83883
  class: ClaudecodeSubagent,
82467
83884
  meta: {
@@ -83084,10 +84501,15 @@ var SubagentsProcessor = class extends FeatureProcessor {
83084
84501
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
83085
84502
  const dirPath = typeof root === "string" ? root : root.relativeDirPath;
83086
84503
  const baseDir = join(rootOutputRoot, dirPath);
83087
- if (forDeletion && await directoryExists(baseDir)) await assertWritablePathInsideRoot({
83088
- rootPath: rootOutputRoot,
83089
- targetPath: baseDir
83090
- });
84504
+ if (forDeletion && await directoryExists(baseDir)) try {
84505
+ await assertWritablePathInsideRoot({
84506
+ rootPath: rootOutputRoot,
84507
+ targetPath: baseDir
84508
+ });
84509
+ } catch (error) {
84510
+ this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(baseDir)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
84511
+ continue;
84512
+ }
83091
84513
  const subagentFilePaths = (await findFilesByGlobs(factory.meta.filePattern, {
83092
84514
  cwd: baseDir,
83093
84515
  followSymbolicLinks: !forDeletion
@@ -84049,6 +85471,16 @@ const AntigravityRuleFrontmatterSchema = z.looseObject({
84049
85471
  description: z.optional(z.string())
84050
85472
  });
84051
85473
  /**
85474
+ * File name of a non-root rule in the shared global `~/.gemini/config/rules/`
85475
+ * directory. Antigravity reads only its top level, so a nested rulesync rule
85476
+ * (`frontend/style.md`) is flattened into a single file name
85477
+ * (`frontend-style.md`). The IDE and the CLI both use it so they write the
85478
+ * same file for the same rule.
85479
+ */
85480
+ function toGlobalRuleFileName(relativeFilePath) {
85481
+ return relativeFilePath.split(/[\\/]/).join("-");
85482
+ }
85483
+ /**
84052
85484
  * Converts a comma-separated globs string or array to an array of globs.
84053
85485
  * @param globs - Comma-separated globs string (e.g., "*.ts,*.js") or array of globs
84054
85486
  * @returns Array of glob patterns
@@ -84279,7 +85711,7 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
84279
85711
  });
84280
85712
  return new AntigravityCliRule({
84281
85713
  ...params,
84282
- relativeFilePath: params.relativeFilePath.split(/[\\/]/).join("-"),
85714
+ relativeFilePath: toGlobalRuleFileName(params.relativeFilePath),
84283
85715
  fileContent,
84284
85716
  global
84285
85717
  });
@@ -84377,10 +85809,14 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
84377
85809
  *
84378
85810
  * - Project scope: every rule is placed as a non-root file in
84379
85811
  * `.agents/rules/` with Antigravity trigger frontmatter.
84380
- * - Global scope: a single plain `~/.gemini/GEMINI.md` root file (shared with
84381
- * the Antigravity CLI), without frontmatter.
85812
+ * - Global scope: a plain `~/.gemini/GEMINI.md` root file without
85813
+ * frontmatter, and non-root rules with trigger frontmatter in
85814
+ * `~/.gemini/config/rules/`. Both are shared with the Antigravity CLI, which
85815
+ * writes the same files for the same rules.
85816
+ *
85817
+ * @see https://antigravity.google/docs/rules
84382
85818
  */
84383
- var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
85819
+ var AntigravityIdeRule = class extends ToolRule {
84384
85820
  frontmatter;
84385
85821
  body;
84386
85822
  constructor({ frontmatter, body, ...rest }) {
@@ -84408,28 +85844,18 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84408
85844
  };
84409
85845
  }
84410
85846
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
84411
- if (global) return { root: this.getGlobalRootPath(excludeToolDir) };
85847
+ if (global) return {
85848
+ root: this.getGlobalRootPath(excludeToolDir),
85849
+ nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_GEMINI_DIR, ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR, excludeToolDir) }
85850
+ };
84412
85851
  return {
84413
85852
  root: this.getProjectRootPath(),
84414
85853
  nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_AGENTS_DIR, "rules", excludeToolDir) }
84415
85854
  };
84416
85855
  }
84417
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
85856
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
84418
85857
  const paths = this.getSettablePaths({ global });
84419
- if (global) {
84420
- const rootPath = paths.root;
84421
- const fileContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
84422
- return new this({
84423
- outputRoot,
84424
- relativeDirPath: rootPath.relativeDirPath,
84425
- relativeFilePath: rootPath.relativeFilePath,
84426
- frontmatter: {},
84427
- body: fileContent,
84428
- validate,
84429
- root: true
84430
- });
84431
- }
84432
- if (relativeFilePath === paths.root.relativeFilePath) {
85858
+ if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
84433
85859
  const rootPath = paths.root;
84434
85860
  const rootContent = await readFileContent(join(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
84435
85861
  return new this({
@@ -84439,10 +85865,10 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84439
85865
  frontmatter: {},
84440
85866
  body: rootContent,
84441
85867
  validate,
84442
- root: true
85868
+ root: true,
85869
+ global
84443
85870
  });
84444
85871
  }
84445
- if (!("nonRoot" in paths) || !paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
84446
85872
  const nonRootDirPath = paths.nonRoot.relativeDirPath;
84447
85873
  const filePath = join(outputRoot, nonRootDirPath, relativeFilePath);
84448
85874
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
@@ -84459,23 +85885,12 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84459
85885
  body,
84460
85886
  frontmatter: parsedFrontmatter,
84461
85887
  validate,
84462
- root: false
85888
+ root: false,
85889
+ global
84463
85890
  });
84464
85891
  }
84465
85892
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
84466
85893
  const paths = this.getSettablePaths({ global });
84467
- if (global) {
84468
- const rootPath = paths.root;
84469
- return new this({
84470
- outputRoot,
84471
- relativeDirPath: rootPath.relativeDirPath,
84472
- relativeFilePath: rootPath.relativeFilePath,
84473
- frontmatter: {},
84474
- body: rulesyncRule.getBody(),
84475
- validate,
84476
- root: true
84477
- });
84478
- }
84479
85894
  if (rulesyncRule.getFrontmatter().root) {
84480
85895
  const rootPath = paths.root;
84481
85896
  return new this({
@@ -84485,7 +85900,8 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84485
85900
  frontmatter: {},
84486
85901
  body: rulesyncRule.getBody(),
84487
85902
  validate,
84488
- root: true
85903
+ root: true,
85904
+ global
84489
85905
  });
84490
85906
  }
84491
85907
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
@@ -84495,16 +85911,16 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84495
85911
  const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
84496
85912
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
84497
85913
  const frontmatter = strategy.generateFrontmatter(normalized, rulesyncFrontmatter);
84498
- if (!("nonRoot" in paths) || !paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
84499
- const kebabCaseFilename = toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
85914
+ const relativeFilePath = global ? toGlobalRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
84500
85915
  return new this({
84501
85916
  outputRoot,
84502
85917
  relativeDirPath: paths.nonRoot.relativeDirPath,
84503
- relativeFilePath: kebabCaseFilename,
85918
+ relativeFilePath,
84504
85919
  frontmatter,
84505
85920
  body: rulesyncRule.getBody(),
84506
85921
  validate,
84507
- root: false
85922
+ root: false,
85923
+ global
84508
85924
  });
84509
85925
  }
84510
85926
  toRulesyncRule() {
@@ -84549,15 +85965,26 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84549
85965
  error: null
84550
85966
  };
84551
85967
  }
85968
+ /**
85969
+ * `~/.gemini/config/rules/` is shared with the Antigravity CLI and holds
85970
+ * global rules the user created outside rulesync, which cannot be told apart
85971
+ * from generated ones, so the orphan sweep never removes files there.
85972
+ */
85973
+ isDeletable() {
85974
+ return !this.global || this.root;
85975
+ }
84552
85976
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
84553
- return new AntigravityIdeRule({
85977
+ const rootPath = this.getSettablePaths({ global }).root;
85978
+ const isRoot = relativeFilePath === rootPath.relativeFilePath && relativeDirPath === rootPath.relativeDirPath;
85979
+ return new this({
84554
85980
  outputRoot,
84555
85981
  relativeDirPath,
84556
85982
  relativeFilePath,
84557
85983
  frontmatter: {},
84558
85984
  body: "",
84559
85985
  validate: false,
84560
- root: global || relativeFilePath === "AGENTS.md" && relativeDirPath === "."
85986
+ root: isRoot,
85987
+ global
84561
85988
  });
84562
85989
  }
84563
85990
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -91238,7 +92665,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
91238
92665
  meta: {
91239
92666
  extension: "md",
91240
92667
  supportsGlobal: true,
91241
- ruleDiscoveryMode: "auto"
92668
+ ruleDiscoveryMode: "auto",
92669
+ sharedGlobalNonRootDir: true
91242
92670
  }
91243
92671
  }],
91244
92672
  ["antigravity-plugin", {
@@ -91875,6 +93303,7 @@ var RulesProcessor = class extends FeatureProcessor {
91875
93303
  toolRules,
91876
93304
  factory
91877
93305
  });
93306
+ await this.warnForWarpLegacyRootFile(toolRules);
91878
93307
  this.warnForInstructionBudget({
91879
93308
  toolRules,
91880
93309
  meta
@@ -92288,6 +93717,24 @@ var RulesProcessor = class extends FeatureProcessor {
92288
93717
  this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
92289
93718
  }
92290
93719
  /**
93720
+ * Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
93721
+ * `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so the
93722
+ * `AGENTS.md` this run writes would never be read. Rulesync neither writes
93723
+ * nor deletes `WARP.md` (it is hand-authored), so the only thing it can do is
93724
+ * say so. Project scope only: the documented precedence is about project
93725
+ * rules, and Warp's global rule source is `~/.agents/AGENTS.md`.
93726
+ * @see https://docs.warp.dev/agents/capabilities/rules/
93727
+ */
93728
+ async warnForWarpLegacyRootFile(toolRules) {
93729
+ if (this.toolTarget !== "warp" || this.global) return;
93730
+ if (toolRules.length === 0) return;
93731
+ const { root } = WarpRule.getSettablePaths();
93732
+ const legacyRelativePath = join(root.relativeDirPath, WARP_LEGACY_RULE_FILE_NAME);
93733
+ if (!await fileExists(join(this.outputRoot, legacyRelativePath))) return;
93734
+ const rootRelativePath = join(root.relativeDirPath, root.relativeFilePath);
93735
+ this.logger.warn(`${legacyRelativePath} exists next to ${rootRelativePath}, and Warp reads WARP.md instead of AGENTS.md when both are present, so the generated ${rootRelativePath} is ignored by Warp. Move any content you still need into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, then delete or rename ${legacyRelativePath}.`);
93736
+ }
93737
+ /**
92291
93738
  * Handle localRoot rule generation based on tool target.
92292
93739
  * - `separate-local-file`: writes a dedicated `*.local.md` root file
92293
93740
  * (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
@@ -93669,26 +95116,64 @@ function createFoldRootOverwriteWatch({ logger }) {
93669
95116
  }
93670
95117
  //#endregion
93671
95118
  //#region src/lib/orphan-sweep.ts
93672
- function createOrphanSweepPlan() {
95119
+ function createOrphanSweepPlan({ logger } = {}) {
93673
95120
  const generatedPaths = /* @__PURE__ */ new Set();
93674
95121
  const generatedTrees = /* @__PURE__ */ new Set();
93675
95122
  const deferredSweeps = [];
93676
95123
  const deletedPathsByFeature = /* @__PURE__ */ new Map();
93677
- const isInsideGeneratedTree = (absolutePath) => {
93678
- let current = absolutePath;
95124
+ const warnedUnresolvable = /* @__PURE__ */ new Set();
95125
+ let landingClaims;
95126
+ const isInsideTree = ({ path, trees }) => {
95127
+ let current = path;
93679
95128
  let parent = dirname(current);
93680
95129
  while (parent !== current) {
93681
- if (generatedTrees.has(parent)) return true;
95130
+ if (trees.has(parent)) return true;
93682
95131
  current = parent;
93683
95132
  parent = dirname(current);
93684
95133
  }
93685
95134
  return false;
93686
95135
  };
95136
+ const followLinks = async (path) => {
95137
+ try {
95138
+ return await writeLandingPath(path);
95139
+ } catch {
95140
+ return null;
95141
+ }
95142
+ };
95143
+ const landingsOf = async (paths) => {
95144
+ const landings = await Promise.all([...paths].map((path) => followLinks(path)));
95145
+ return new Set(landings.filter((landing) => landing !== null));
95146
+ };
95147
+ const getLandingClaims = () => {
95148
+ landingClaims ??= Promise.all([landingsOf(generatedPaths), landingsOf(generatedTrees)]).then(([paths, trees]) => ({
95149
+ paths,
95150
+ trees: new Set([...trees].filter((tree) => dirname(tree) !== tree))
95151
+ }));
95152
+ return landingClaims;
95153
+ };
95154
+ const landsOnClaim = async ({ path, exactly }) => {
95155
+ const resolved = resolve(path);
95156
+ const landing = await followLinks(resolved);
95157
+ if (landing === null) {
95158
+ if (!warnedUnresolvable.has(resolved)) {
95159
+ warnedUnresolvable.add(resolved);
95160
+ logger?.warn(`Refusing to sweep ${quoteForLog(resolved)}: its symbolic links cannot be followed (a link cycle or an unreadable directory)`);
95161
+ }
95162
+ return true;
95163
+ }
95164
+ const claims = await getLandingClaims();
95165
+ return claims.paths.has(landing) || !exactly && isInsideTree({
95166
+ path: landing,
95167
+ trees: claims.trees
95168
+ });
95169
+ };
93687
95170
  const plan = {
93688
95171
  registerGenerated({ paths }) {
95172
+ landingClaims = void 0;
93689
95173
  for (const path of paths) generatedPaths.add(resolve(path));
93690
95174
  },
93691
95175
  registerGeneratedTree({ paths }) {
95176
+ landingClaims = void 0;
93692
95177
  for (const path of paths) {
93693
95178
  const resolved = resolve(path);
93694
95179
  generatedPaths.add(resolved);
@@ -93697,13 +95182,26 @@ function createOrphanSweepPlan() {
93697
95182
  },
93698
95183
  isGenerated({ path }) {
93699
95184
  const resolved = resolve(path);
93700
- return generatedPaths.has(resolved) || isInsideGeneratedTree(resolved);
95185
+ return generatedPaths.has(resolved) || isInsideTree({
95186
+ path: resolved,
95187
+ trees: generatedTrees
95188
+ });
93701
95189
  },
93702
- isGeneratedExactly({ path }) {
93703
- return generatedPaths.has(resolve(path));
95190
+ async isGeneratedExactly({ path }) {
95191
+ return generatedPaths.has(resolve(path)) || await landsOnClaim({
95192
+ path,
95193
+ exactly: true
95194
+ });
93704
95195
  },
93705
- rejectClaimed({ items, getPath }) {
93706
- return items.filter((item) => !plan.isGenerated({ path: getPath(item) }));
95196
+ async rejectClaimed({ items, getPath }) {
95197
+ const claimed = await Promise.all(items.map(async (item) => {
95198
+ const path = getPath(item);
95199
+ return plan.isGenerated({ path }) || await landsOnClaim({
95200
+ path,
95201
+ exactly: false
95202
+ });
95203
+ }));
95204
+ return items.filter((_, index) => !claimed[index]);
93707
95205
  },
93708
95206
  defer({ sweep, reportDeleted }) {
93709
95207
  deferredSweeps.push({
@@ -93897,19 +95395,19 @@ function scheduleRetiredTargetSweeps({ config, logger, sweepPlan, sourceLoadFail
93897
95395
  }
93898
95396
  }
93899
95397
  async function sweepFiles({ processor, sweepPlan, unownedPaths }) {
93900
- const candidates = sweepPlan.rejectClaimed({
95398
+ const candidates = (await sweepPlan.rejectClaimed({
93901
95399
  items: await processor.loadToolFiles({ forDeletion: true }),
93902
95400
  getPath: (f) => f.getFilePath()
93903
- }).filter((f) => !unownedPaths.has(resolve(f.getFilePath())));
95401
+ })).filter((f) => !unownedPaths.has(resolve(f.getFilePath())));
93904
95402
  const present = await Promise.all(candidates.map((f) => fileExists(f.getFilePath())));
93905
95403
  return await processor.removeOrphanAiFiles(candidates.filter((_, index) => present[index]), []) > 0;
93906
95404
  }
93907
95405
  async function sweepDirs({ processor, sweepPlan }) {
93908
- return await processor.removeOrphanAiDirs(sweepPlan.rejectClaimed({
95406
+ return await processor.removeOrphanAiDirs(await sweepPlan.rejectClaimed({
93909
95407
  items: await processor.loadToolDirsToDelete(),
93910
95408
  getPath: (d) => d.getDirPath()
93911
95409
  }), []) + await processor.removeOrphanFlatFiles({
93912
- existingFlatFiles: sweepPlan.rejectClaimed({
95410
+ existingFlatFiles: await sweepPlan.rejectClaimed({
93913
95411
  items: await processor.loadToolFlatFilesToDelete(),
93914
95412
  getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
93915
95413
  }),
@@ -94096,7 +95594,7 @@ async function processFeatureGeneration(params) {
94096
95594
  if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
94097
95595
  sweep: async () => {
94098
95596
  const existingToolFiles = await processor.loadToolFiles({ forDeletion: true });
94099
- return await processor.removeOrphanAiFiles(sweepPlan.rejectClaimed({
95597
+ return await processor.removeOrphanAiFiles(await sweepPlan.rejectClaimed({
94100
95598
  items: existingToolFiles,
94101
95599
  getPath: (f) => f.getFilePath()
94102
95600
  }), toolFiles) > 0;
@@ -94110,6 +95608,16 @@ async function processFeatureGeneration(params) {
94110
95608
  sourceLoadFailed: processor.hasRulesyncSourceLoadFailure()
94111
95609
  };
94112
95610
  }
95611
+ /**
95612
+ * Whether a write to `targetPath` lands strictly below `rootPath` once every
95613
+ * link on the way to either is followed. A cycle on either side answers false.
95614
+ */
95615
+ async function landsStrictlyBelowRoot({ rootPath, targetPath }) {
95616
+ const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
95617
+ if (rootLanding === null || targetLanding === null) return false;
95618
+ const relativePath = relative(rootLanding, targetLanding);
95619
+ return relativePath !== "" && !pathEscapesRoot(relativePath);
95620
+ }
94113
95621
  async function processDirFeatureGeneration(params) {
94114
95622
  const { config, processor, toolDirs, sweepPlan } = params;
94115
95623
  let totalCount = 0;
@@ -94120,7 +95628,11 @@ async function processDirFeatureGeneration(params) {
94120
95628
  allPaths.push(...writeResult.paths);
94121
95629
  if (writeResult.count > 0) hasDiff = true;
94122
95630
  const ownedTrees = toolDirs.filter((d) => d.ownsDirTree());
94123
- sweepPlan.registerGeneratedTree({ paths: ownedTrees.map((d) => d.getDirPath()) });
95631
+ const treesBelowRoot = await Promise.all(ownedTrees.map((d) => landsStrictlyBelowRoot({
95632
+ rootPath: d.getOutputRoot(),
95633
+ targetPath: d.getDirPath()
95634
+ })));
95635
+ sweepPlan.registerGeneratedTree({ paths: ownedTrees.filter((_, index) => treesBelowRoot[index]).map((d) => d.getDirPath()) });
94124
95636
  sweepPlan.registerGenerated({ paths: toolDirs.flatMap((d) => {
94125
95637
  const dirPath = d.getDirPath();
94126
95638
  const mainFile = d.getMainFile();
@@ -94133,13 +95645,13 @@ async function processDirFeatureGeneration(params) {
94133
95645
  if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
94134
95646
  sweep: async () => {
94135
95647
  const existingToolDirs = await processor.loadToolDirsToDelete();
94136
- const orphanDirCount = await processor.removeOrphanAiDirs(sweepPlan.rejectClaimed({
95648
+ const orphanDirCount = await processor.removeOrphanAiDirs(await sweepPlan.rejectClaimed({
94137
95649
  items: existingToolDirs,
94138
95650
  getPath: (d) => d.getDirPath()
94139
95651
  }), toolDirs);
94140
95652
  const existingFlatFiles = await processor.loadToolFlatFilesToDelete();
94141
95653
  const orphanFileCount = await processor.removeOrphanFlatFiles({
94142
- existingFlatFiles: sweepPlan.rejectClaimed({
95654
+ existingFlatFiles: await sweepPlan.rejectClaimed({
94143
95655
  items: existingFlatFiles,
94144
95656
  getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
94145
95657
  }),
@@ -94166,10 +95678,10 @@ async function processEmptyFeatureGeneration(params) {
94166
95678
  if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
94167
95679
  sweep: async () => {
94168
95680
  const existingToolFiles = await processor.loadToolFiles({ forDeletion: true });
94169
- const filesToDelete = sweepPlan.rejectClaimed({
95681
+ const filesToDelete = (await sweepPlan.rejectClaimed({
94170
95682
  items: existingToolFiles,
94171
95683
  getPath: (f) => f.getFilePath()
94172
- }).filter((f) => !skipFilePaths?.has(f.getRelativePathFromCwd()));
95684
+ })).filter((f) => !skipFilePaths?.has(f.getRelativePathFromCwd()));
94173
95685
  return await processor.removeOrphanAiFiles(filesToDelete, []) > 0;
94174
95686
  },
94175
95687
  reportDeleted: () => processor.getRemovedPaths()
@@ -94495,7 +96007,7 @@ async function generate(params) {
94495
96007
  logger
94496
96008
  });
94497
96009
  let skillsResult;
94498
- const sweepPlan = createOrphanSweepPlan();
96010
+ const sweepPlan = createOrphanSweepPlan({ logger });
94499
96011
  const runners = {
94500
96012
  ignore: () => generateIgnoreCore({
94501
96013
  config,
@@ -95634,6 +97146,6 @@ async function importChecksCore(params) {
95634
97146
  return writtenCount;
95635
97147
  }
95636
97148
  //#endregion
95637
- export { getRulesyncSourceCandidates as $, removeFileStrict as $t, CLAUDECODE_LOCAL_RULE_FILE_NAME as A, RULESYNC_RULES_RELATIVE_DIR_PATH as An, assertDirectoryIfExists as At, AUGMENTCODE_DIR as B, formatError as Bn, getHomeDirectory as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Cn, warnOnConflictingFlags as Ct, CODEXCLI_BASH_RULES_FILE_NAME as D, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Dn, CLIError as Dt, ChecksProcessor as E, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as En, withWarnOnceScope as Et, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as F, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Fn, directoryExists as Ft, RulesyncSkill as G, stripControlCharacters as Gn, listFilePathsRecursively as Gt, getLocalSkillDirNames as H, hasDeceptiveHiddenCharacters as Hn, isFileSystemError as Ht, caseFoldIdentity as I, parseCommaSeparatedList as In, ensureDir as It, RulesyncRuleFrontmatterSchema as J, readFileContent as Jt, RulesyncSkillFrontmatterSchema as K, stripControlCharactersKeepingLineFeeds as Kn, listSubdirectoryNames as Kt, groupSpellingsByCaseFoldedIdentity as L, ALL_FEATURES as Ln, fileExists as Lt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as M, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Mn, assertWritablePathInsideRoot as Mt, CLAUDECODE_SKILLS_DIR_PATH as N, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Nn, checkPathTraversal as Nt, CODEXCLI_DIR as O, RULESYNC_PERMISSIONS_SCHEMA_URL as On, ErrorCodes as Ot, FACTORYDROID_DIR as P, RULESYNC_USER_CONFIG_DIR_NAME as Pn, createTempDirectory as Pt, RulesyncHooks as Q, removeFile as Qt, QWENCODE_DIR as R, ALL_FEATURES_WITH_WILDCARD as Rn, findFilesByGlobs as Rt, COMMANDCODE_DIR as S, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Sn, fallbackLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_SCHEMA_URL as Tn, resetRunWarningState as Tt, RulesyncSubagent as U, hasEnclosingMarkOutsideKeycap as Un, isSymlink as Ut, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as V, truncateText as Vn, isFileNotFoundError as Vt, RulesyncSubagentFrontmatterSchema as W, quoteForLog as Wn, listDirectoryEntryNames as Wt, RulesyncMcp as X, removeDirectory as Xt, RulesyncPermissions as Y, readFileContentOrNull as Yt, RulesyncIgnore as Z, removeDirectoryStrict as Zt, LETTACODE_DIR as _, RULESYNC_CONFIG_RELATIVE_FILE_PATH as _n, assertTargetsFeaturesExclusive as _t, getProcessorRegistryEntry as a, writeFileBuffer as an, RulesyncCheckFrontmatterSchema as at, CommandsProcessor as b, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as bn, JsonLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS_WITH_WILDCARD as cn, SHARED_USER_MANAGED_CONFIG_PATHS as ct, ELLIPSIS_WIDTH as d, CURATED_RULES_FEATURE_SUBDIR as dn, mergeInputRootConfigs as dt, removeTempDirectory as en, resolveRulesyncSourceWritePath as et, displayWidthOf as f, FEATURE_SOURCE_TREE_ENTRIES as fn, resolveEffectiveInputRoots as ft, HooksProcessor as g, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as gn, SourceEntrySchema as gt, IgnoreProcessor as h, RULESYNC_CHECKS_RELATIVE_DIR_PATH as hn, GITIGNORE_DESTINATION_KEY as ht, inspectInputRoots as i, toPosixPath as in, RulesyncCheck as it, CLAUDECODE_MEMORIES_DIR_NAME as j, RULESYNC_SKILLS_RELATIVE_DIR_PATH as jn, assertTreeContainsNoSymlinks as jt, CLAUDECODE_DIR as k, RULESYNC_RELATIVE_DIR_PATH as kn, applyFileMode as kt, SubagentsProcessor as l, PACKAGING_TOOL_TARGETS as ln, SKILL_FILE_NAME as lt, McpProcessor as m, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as mn, ConfigFileSchema as mt, formatSourceLoadFailure as n, runWithDirectoryRollback as nn, RulesyncCommand as nt, convertFromTool as o, writeFileContent as on, stringifyFrontmatter as ot, shortenToWidth as p, MAX_FILE_SIZE as pn, CONFLICTING_TARGET_PAIRS as pt, RulesyncRule as q, stripHiddenCharacters as qn, pathEscapesRoot as qt, generate as r, splitPathSegments as rn, RulesyncCommandFrontmatterSchema as rt, isPackagingToolTarget as s, ALL_TOOL_TARGETS as sn, loadYaml as st, importFromTool as t, resolvePath as tn, parseJsonc as tt, SkillsProcessor as u, ToolTargetSchema as un, ConfigResolver as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_SCHEMA_URL as vn, findControlCharacter as vt, CODEBUDDY_DIR as w, RULESYNC_MCP_RELATIVE_FILE_PATH as wn, withFallbackLoggerTarget as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_HOOKS_RELATIVE_FILE_PATH as xn, WarningCollectingLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as yn, ConsoleLogger as yt, QWENCODE_LOCAL_RULE_FILE_NAME as z, DEPRECATED_FEATURE_REPLACEMENTS as zn, getFileSize as zt };
97149
+ export { RulesyncHooks as $, removeFile as $t, CLAUDECODE_DIR as A, RULESYNC_RELATIVE_DIR_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, DEPRECATED_FEATURE_REPLACEMENTS as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, CODEXCLI_BASH_RULES_FILE_NAME as D, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_MCP_SCHEMA_URL as En, resetRunWarningState as Et, FACTORYDROID_DIR as F, RULESYNC_USER_CONFIG_DIR_NAME as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, quoteForLog as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, truncateText as Hn, isFileNotFoundError as Ht, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as I, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as In, directoryExists as It, RulesyncRule as J, stripHiddenCharacters as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, stripControlCharacters as Kn, listFilePathsRecursively as Kt, caseFoldIdentity as L, parseCommaSeparatedList as Ln, ensureDir as Lt, CLAUDECODE_MEMORIES_DIR_NAME as M, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as N, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Nn, assertWritablePathInsideRoot as Nt, CODEXCLI_DIR as O, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_SKILLS_DIR_PATH as P, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectoryStrict as Qt, groupSpellingsByCaseFoldedIdentity as R, ALL_FEATURES as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, hasDeceptiveHiddenCharacters as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, formatError as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, hasEnclosingMarkOutsideKeycap as Wn, isSymlink as Wt, RulesyncPermissions as X, readFileContentOrNull as Xt, RulesyncRuleFrontmatterSchema as Y, readFileContent as Yt, RulesyncMcp as Z, removeDirectory as Zt, LETTACODE_DIR as _, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, toPosixPath as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ToolTargetSchema as dn, ConfigResolver as dt, removeFileStrict as en, getRulesyncSourceCandidates as et, displayWidthOf as f, CURATED_RULES_FEATURE_SUBDIR as fn, mergeInputRootConfigs as ft, HooksProcessor as g, RULESYNC_CHECKS_RELATIVE_DIR_PATH as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as hn, ConfigFileSchema as ht, inspectInputRoots as i, splitPathSegments as in, RulesyncCommandFrontmatterSchema as it, CLAUDECODE_LOCAL_RULE_FILE_NAME as j, RULESYNC_RULES_RELATIVE_DIR_PATH as jn, assertDirectoryIfExists as jt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as k, RULESYNC_PERMISSIONS_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, ALL_TOOL_TARGETS_WITH_WILDCARD as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, MAX_FILE_SIZE as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, resolvePath as nn, parseJsonc as nt, convertFromTool as o, writeFileBuffer as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, FEATURE_SOURCE_TREE_ENTRIES as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, stripControlCharactersKeepingLineFeeds as qn, listSubdirectoryNames as qt, generate as r, runWithDirectoryRollback as rn, RulesyncCommand as rt, isPackagingToolTarget as s, writeFileContent as sn, stringifyFrontmatter as st, importFromTool as t, removeTempDirectory as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, PACKAGING_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CONFIG_SCHEMA_URL as yn, findControlCharacter as yt, QWENCODE_DIR as z, ALL_FEATURES_WITH_WILDCARD as zn, findFilesByGlobs as zt };
95638
97150
 
95639
- //# sourceMappingURL=import-CFKjn6d-.js.map
97151
+ //# sourceMappingURL=import-CnhfUGo7.js.map