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.
@@ -444,36 +444,36 @@ const isFeatureValueEnabled = (value) => {
444
444
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
445
445
  //#endregion
446
446
  //#region src/constants/rulesync-paths.ts
447
- const { join: join$408 } = node_path.posix;
447
+ const { join: join$410 } = node_path.posix;
448
448
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
449
449
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
450
450
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
451
451
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
452
452
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
453
453
  const RULES_FEATURE_SUBDIR = "rules";
454
- const CURATED_RULES_FEATURE_SUBDIR = join$408(RULES_FEATURE_SUBDIR, ".curated");
454
+ const CURATED_RULES_FEATURE_SUBDIR = join$410(RULES_FEATURE_SUBDIR, ".curated");
455
455
  const COMMANDS_FEATURE_SUBDIR = "commands";
456
456
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
457
457
  const CHECKS_FEATURE_SUBDIR = "checks";
458
458
  const SKILLS_FEATURE_SUBDIR = "skills";
459
- const CURATED_SKILLS_FEATURE_SUBDIR = join$408(SKILLS_FEATURE_SUBDIR, ".curated");
460
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
- join$408(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
459
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$410(SKILLS_FEATURE_SUBDIR, ".curated");
460
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
+ join$410(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
471
471
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
472
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
472
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
473
473
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
474
474
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
475
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$408(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
475
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$410(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
477
477
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
478
478
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
479
479
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -712,6 +712,7 @@ const subagentsProcessorToolTargetTuple = [
712
712
  "antigravity-plugin",
713
713
  "augmentcode",
714
714
  "augmentcode-plugin",
715
+ "bob",
715
716
  "claudecode",
716
717
  "claudecode-plugin",
717
718
  "claudecode-legacy",
@@ -878,6 +879,7 @@ const permissionsProcessorToolTargetTuple = [
878
879
  "claudecode",
879
880
  "cline",
880
881
  "codebuddy",
882
+ "codewhale",
881
883
  "codexcli",
882
884
  "commandcode",
883
885
  "continue",
@@ -899,6 +901,7 @@ const permissionsProcessorToolTargetTuple = [
899
901
  "kiro-ide",
900
902
  "lettacode",
901
903
  "mimocode",
904
+ "omp",
902
905
  "opencode",
903
906
  "pi",
904
907
  "pool",
@@ -922,6 +925,7 @@ const checksProcessorToolTargetTuple = [
922
925
  "factorydroid",
923
926
  "gitlabduo",
924
927
  "hermesagent",
928
+ "kilo",
925
929
  "qwencode",
926
930
  "rovodev",
927
931
  "takt"
@@ -3716,6 +3720,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3716
3720
  ".grok/sandbox.toml",
3717
3721
  ".letta/settings.json",
3718
3722
  ".qoder/settings.json",
3723
+ ".omp/config.yml",
3724
+ ".omp/agent/config.yml",
3719
3725
  ".rovodev/config.yml",
3720
3726
  ".rovodev/mcp.json",
3721
3727
  ".poolside/settings.yaml",
@@ -7689,6 +7695,32 @@ const ToolScopedPermissionSchema = zod_mini.z.record(PermissionCategorySchema, P
7689
7695
  * added without a schema break.
7690
7696
  */
7691
7697
  const CanonicalPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(ToolScopedPermissionSchema) });
7698
+ /**
7699
+ * One Codewhale `[[rules]]` record of `~/.codewhale/permissions.toml`, written
7700
+ * verbatim. Codewhale parses the file with unknown fields denied, so a single
7701
+ * misspelled key would make it reject every rule in the file; the record is
7702
+ * therefore strict here, surfacing the typo on the rulesync source instead.
7703
+ *
7704
+ * @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
7705
+ */
7706
+ const CodewhalePermissionRuleSchema = zod_mini.z.strictObject({
7707
+ tool: zod_mini.z.string(),
7708
+ command: zod_mini.z.optional(zod_mini.z.string()),
7709
+ command_exact: zod_mini.z.optional(zod_mini.z.boolean()),
7710
+ path: zod_mini.z.optional(zod_mini.z.string()),
7711
+ workspace: zod_mini.z.optional(zod_mini.z.string()),
7712
+ action: zod_mini.z.optional(PermissionActionSchema)
7713
+ });
7714
+ /**
7715
+ * Tool-scoped override block for Codewhale. `rules` carries native records the
7716
+ * canonical category/pattern shape cannot express — a `workspace`-scoped rule,
7717
+ * a tool rulesync does not map, an exact-command deny — and is emitted ahead of
7718
+ * the rules converted from the canonical block.
7719
+ */
7720
+ const CodewhalePermissionsOverrideSchema = zod_mini.z.looseObject({
7721
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
7722
+ rules: zod_mini.z.optional(zod_mini.z.array(CodewhalePermissionRuleSchema))
7723
+ });
7692
7724
  const KimiCodePermissionsOverrideSchema = zod_mini.z.looseObject({
7693
7725
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
7694
7726
  defaultPermissionMode: zod_mini.z.optional(zod_mini.z.enum([
@@ -7909,12 +7941,15 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
7909
7941
  * autonomy/sandbox controls with no canonical permission category — under
7910
7942
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
7911
7943
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
7912
- * `todoWrite`, `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
7913
- * `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
7914
- * `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
7944
+ * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
7945
+ * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
7946
+ * `allowedInsecureVoiceBaseUrls`). Qwen Code strips `tools.workflowsEnabled`,
7947
+ * `tools.executionSandbox`, `security.allowPrivateNetworkHooks` and
7915
7948
  * `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
7916
- * skips those three in project scope and announces a granting value in global
7917
- * scope. `security.allowedHttpHookUrls` (honored in a workspace only while no
7949
+ * skips those four in project scope and announces a granting value in global
7950
+ * scope (any `executionSandbox` change counts). `tools.workflowNameOnly` is
7951
+ * written in both scopes, but a workspace may only turn it on, which the
7952
+ * project-scope note says. `security.allowedHttpHookUrls` (honored in a workspace only while no
7918
7953
  * higher scope sets it) and `security.folderTrust` (the initial trust decision
7919
7954
  * is made from user/system settings alone, before the workspace merge) are
7920
7955
  * written in both scopes, with a note in project scope and an announcement of
@@ -8802,6 +8837,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
8802
8837
  augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
8803
8838
  kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
8804
8839
  codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
8840
+ codewhale: zod_mini.z.optional(CodewhalePermissionsOverrideSchema),
8805
8841
  zed: zod_mini.z.optional(ZedPermissionsOverrideSchema),
8806
8842
  devin: zod_mini.z.optional(DevinPermissionsOverrideSchema),
8807
8843
  "antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
@@ -8814,6 +8850,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
8814
8850
  copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8815
8851
  crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8816
8852
  goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8853
+ omp: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8817
8854
  pool: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8818
8855
  qoder: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8819
8856
  grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
@@ -10747,6 +10784,13 @@ const RulesyncSubagentFrontmatterSchema = zod_mini.z.looseObject({
10747
10784
  customInstructions: zod_mini.z.optional(zod_mini.z.string()),
10748
10785
  groups: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.unknown()))
10749
10786
  })),
10787
+ bob: zod_mini.z.optional(zod_mini.z.looseObject({
10788
+ slug: zod_mini.z.optional(zod_mini.z.string()),
10789
+ whenToUse: zod_mini.z.optional(zod_mini.z.string()),
10790
+ roleDefinition: zod_mini.z.optional(zod_mini.z.string()),
10791
+ customInstructions: zod_mini.z.optional(zod_mini.z.string()),
10792
+ groups: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.unknown()))
10793
+ })),
10750
10794
  zoocode: zod_mini.z.optional(zod_mini.z.looseObject({
10751
10795
  /** Per-mode MCP server allowlist (Zoo Code v3.60.0+); omitted = all. */
10752
10796
  allowedMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
@@ -11087,6 +11131,27 @@ const GITLABDUO_SKILLS_DIR = "skills";
11087
11131
  /** User skills (requires `--enable-global-skills` / `GITLAB_ENABLE_GLOBAL_SKILLS`). */
11088
11132
  const GITLABDUO_GLOBAL_SKILLS_DIR = (0, node_path.join)(GITLABDUO_DIR, "skills");
11089
11133
  //#endregion
11134
+ //#region src/constants/kilo-paths.ts
11135
+ const KILO_DIR = ".kilo";
11136
+ const KILO_GLOBAL_DIR = (0, node_path.join)(".config", "kilo");
11137
+ const KILO_RULES_DIR_NAME = "rules";
11138
+ const KILO_COMMANDS_DIR_PATH = (0, node_path.join)(KILO_DIR, "commands");
11139
+ const KILO_GLOBAL_COMMANDS_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "commands");
11140
+ const KILO_SKILLS_DIR_PATH = (0, node_path.join)(KILO_DIR, "skills");
11141
+ const KILO_AGENTS_DIR_PATH = (0, node_path.join)(KILO_DIR, "agents");
11142
+ const KILO_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "agents");
11143
+ const KILO_PLUGINS_DIR_PATH = (0, node_path.join)(KILO_DIR, "plugins");
11144
+ const KILO_GLOBAL_PLUGINS_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "plugins");
11145
+ const KILO_PLUGIN_DIR_PATH = (0, node_path.join)(KILO_DIR, "plugin");
11146
+ const KILO_GLOBAL_PLUGIN_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "plugin");
11147
+ const KILO_RULE_FILE_NAME = "AGENTS.md";
11148
+ const KILO_IGNORE_FILE_NAME = ".kilocodeignore";
11149
+ const KILO_LEGACY_GLOBAL_DIR = ".kilocode";
11150
+ const KILO_JSONC_FILE_NAME = "kilo.jsonc";
11151
+ const KILO_JSON_FILE_NAME = "kilo.json";
11152
+ const KILO_HOOKS_FILE_NAME = "rulesync-hooks.js";
11153
+ const KILO_REVIEW_FILE_NAME = "REVIEW.md";
11154
+ //#endregion
11090
11155
  //#region src/constants/qwencode-paths.ts
11091
11156
  const QWENCODE_DIR = ".qwen";
11092
11157
  const QWENCODE_COMMANDS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "commands");
@@ -11478,8 +11543,14 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
11478
11543
  async removeOrphanAiFiles(existingFiles, generatedFiles) {
11479
11544
  const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
11480
11545
  const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
11546
+ let removedCount = 0;
11481
11547
  for (const aiFile of orphanFiles) {
11482
11548
  const filePath = aiFile.getFilePath();
11549
+ if (await refusesDeleteOutsideRoot({
11550
+ logger: this.logger,
11551
+ rootPath: aiFile.getOutputRoot(),
11552
+ filePath
11553
+ })) continue;
11483
11554
  const loggedPath = stripControlCharacters(filePath);
11484
11555
  if (this.dryRun) this.logger.info(`[DRY RUN] Would delete: ${loggedPath}`);
11485
11556
  else {
@@ -11490,8 +11561,9 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
11490
11561
  path: aiFile.getRelativePathFromCwd(),
11491
11562
  kind: "file"
11492
11563
  });
11564
+ removedCount++;
11493
11565
  }
11494
- return orphanFiles.length;
11566
+ return removedCount;
11495
11567
  }
11496
11568
  /**
11497
11569
  * The paths {@link removeOrphanAiFiles} deleted, or would have deleted under
@@ -11599,6 +11671,25 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
11599
11671
  return true;
11600
11672
  }
11601
11673
  /**
11674
+ * Whether deleting `filePath` has to be refused because a link on the way to
11675
+ * the directory holding it leads out of `rootPath`, warning about the refusal.
11676
+ *
11677
+ * Deletion candidates are enumerated without following links *inside* the
11678
+ * scanned directory, but the scanned directory itself is reached through
11679
+ * whatever links its path contains: a checked-out repository can carry
11680
+ * `.cursor/commands -> ~/notes`, and every file in `~/notes` would then read as
11681
+ * an orphan. Only the parent is judged, because removing a file that is itself
11682
+ * a link unlinks the link where it is spelled and never touches its target.
11683
+ */
11684
+ async function refusesDeleteOutsideRoot({ logger, rootPath, filePath }) {
11685
+ if (!await writablePathEscapesRoot({
11686
+ rootPath,
11687
+ targetPath: (0, node_path.dirname)(filePath)
11688
+ })) return false;
11689
+ logger.warn(`Refusing to delete ${quoteForLog(filePath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
11690
+ return true;
11691
+ }
11692
+ /**
11602
11693
  * {@link refusesWriteOutsideRoot} over every path a single unit of output
11603
11694
  * writes, stopping at the first refusal: a directory written as one unit is
11604
11695
  * held back whole when any one file in it would land outside the root.
@@ -12741,7 +12832,7 @@ function stripSkillFrontmatter(fileContent) {
12741
12832
  content = rest.slice(closing.index + closing[0].length).replace(LEADING_BLANK_LINE_PATTERN, "");
12742
12833
  }
12743
12834
  }
12744
- function handWrittenWarning({ filePath, displayName, toolTarget }) {
12835
+ function handWrittenWarning$1({ filePath, displayName, toolTarget }) {
12745
12836
  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.`;
12746
12837
  }
12747
12838
  /**
@@ -12798,7 +12889,7 @@ var FactorydroidCheck = class extends AggregatedToolCheck {
12798
12889
  toolTarget: "factorydroid",
12799
12890
  fallbackCheckName: FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME,
12800
12891
  handWrittenPreamble: "skip",
12801
- handWrittenWarning,
12892
+ handWrittenWarning: handWrittenWarning$1,
12802
12893
  transformImportedContent: stripSkillFrontmatter
12803
12894
  };
12804
12895
  }
@@ -13279,6 +13370,51 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
13279
13370
  }
13280
13371
  };
13281
13372
  //#endregion
13373
+ //#region src/features/checks/kilo-check.ts
13374
+ function handWrittenWarning({ filePath, displayName, toolTarget }) {
13375
+ 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.`;
13376
+ }
13377
+ /**
13378
+ * Checks adapter for Kilo Code Reviews' repository guidance (root `REVIEW.md`).
13379
+ *
13380
+ * The Kilo review agent reads one plain-Markdown file at the repository root
13381
+ * (from the PR/MR base branch, once "Use REVIEW.md" is enabled in the Kilo web
13382
+ * app), so every `.rulesync/checks/*.md` targeting Kilo collapses into it via
13383
+ * the `fromRulesyncChecks` on {@link AggregatedToolCheck}, with each check
13384
+ * written as a marked section (see `aggregated-check-file.ts` for the marker
13385
+ * convention).
13386
+ *
13387
+ * The reviewer reads the file as free prose, so a check's `severity` and
13388
+ * `tools` have no equivalent there: they are not written and do not come back
13389
+ * on import. Neither does `description` whenever the check also has a body.
13390
+ *
13391
+ * Project scope only — the reviewer reads the file from the repository and
13392
+ * there is no user-level equivalent.
13393
+ *
13394
+ * A root-level `REVIEW.md` is a generic name other reviewers may read too, so
13395
+ * unlike Cursor Bugbot's tool-specific `.cursor/BUGBOT.md` a hand-written file
13396
+ * here is left untouched (the `skip` policy) rather than replaced.
13397
+ *
13398
+ * @see https://kilo.ai/docs/automate/code-reviews
13399
+ */
13400
+ var KiloCheck = class extends AggregatedToolCheck {
13401
+ static getSettablePaths(_options = {}) {
13402
+ return {
13403
+ relativeDirPath: ".",
13404
+ relativeFilePath: KILO_REVIEW_FILE_NAME
13405
+ };
13406
+ }
13407
+ static getAggregatedCheckConfig() {
13408
+ return {
13409
+ displayName: "Kilo Code",
13410
+ toolTarget: "kilo",
13411
+ fallbackCheckName: "review",
13412
+ handWrittenPreamble: "skip",
13413
+ handWrittenWarning
13414
+ };
13415
+ }
13416
+ };
13417
+ //#endregion
13282
13418
  //#region src/features/checks/qwencode-check.ts
13283
13419
  /**
13284
13420
  * Checks adapter for Qwen Code's `/review` project rules (`.qwen/review-rules.md`).
@@ -13375,6 +13511,12 @@ const CLAUDECODE_LOCAL_RULE_FILE_NAME = "CLAUDE.local.md";
13375
13511
  const CLAUDECODE_RULES_DIR_NAME = "rules";
13376
13512
  /** Memories directory name under `.claude/` (legacy format). */
13377
13513
  const CLAUDECODE_MEMORIES_DIR_NAME = "memories";
13514
+ /**
13515
+ * Directory under `.claude/` where Claude Code keeps the memory of a subagent
13516
+ * declared with `memory: local`; documented as not checked into version control.
13517
+ * @see https://code.claude.com/docs/en/sub-agents#enable-persistent-memory
13518
+ */
13519
+ const CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME = "agent-memory-local";
13378
13520
  const CLAUDECODE_COMMANDS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "commands");
13379
13521
  const CLAUDECODE_AGENTS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "agents");
13380
13522
  const CLAUDECODE_SKILLS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "skills");
@@ -14555,6 +14697,22 @@ const SHARED_CONFIG_OWNERSHIP = {
14555
14697
  }
14556
14698
  }
14557
14699
  },
14700
+ ".omp/config.yml": {
14701
+ format: "yaml",
14702
+ invalidRootPolicy: "error",
14703
+ features: { permissions: {
14704
+ kind: "replace-owned-keys",
14705
+ ownedKeys: ["tools", "bash"]
14706
+ } }
14707
+ },
14708
+ ".omp/agent/config.yml": {
14709
+ format: "yaml",
14710
+ invalidRootPolicy: "error",
14711
+ features: { permissions: {
14712
+ kind: "replace-owned-keys",
14713
+ ownedKeys: ["tools", "bash"]
14714
+ } }
14715
+ },
14558
14716
  ".pi/settings.json": {
14559
14717
  format: "json",
14560
14718
  invalidRootPolicy: "error",
@@ -15601,6 +15759,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
15601
15759
  filePattern: "*.json"
15602
15760
  }
15603
15761
  }],
15762
+ ["kilo", {
15763
+ class: KiloCheck,
15764
+ meta: {
15765
+ supportsGlobal: false,
15766
+ filePattern: KILO_REVIEW_FILE_NAME,
15767
+ committedOutput: true
15768
+ }
15769
+ }],
15604
15770
  ["qwencode", {
15605
15771
  class: QwencodeCheck,
15606
15772
  meta: {
@@ -16382,6 +16548,7 @@ const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
16382
16548
  const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
16383
16549
  const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
16384
16550
  const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
16551
+ const ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR = (0, node_path.join)(ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "rules");
16385
16552
  const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
16386
16553
  const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
16387
16554
  const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
@@ -16660,6 +16827,7 @@ const BOB_COMMANDS_DIR_PATH = (0, node_path.join)(BOB_DIR, "commands");
16660
16827
  const BOB_SKILLS_DIR_PATH = (0, node_path.join)(BOB_DIR, "skills");
16661
16828
  const BOB_SETTINGS_FILE_NAME = "settings.json";
16662
16829
  const BOB_GLOBAL_SETTINGS_DIR_PATH = (0, node_path.join)(BOB_DIR, "settings");
16830
+ const BOB_CUSTOM_MODES_FILE_NAME = "custom_modes.yaml";
16663
16831
  //#endregion
16664
16832
  //#region src/features/commands/bob-command.ts
16665
16833
  /**
@@ -17189,6 +17357,7 @@ const CODEWHALE_COMMANDS_DIR_PATH = (0, node_path.join)(CODEWHALE_DIR, "commands
17189
17357
  const CODEWHALE_HOOKS_FILE_NAME = "hooks.toml";
17190
17358
  const CODEWHALE_CONFIG_FILE_NAME = "config.toml";
17191
17359
  const CODEWHALE_MCP_FILE_NAME = "mcp.json";
17360
+ const CODEWHALE_PERMISSIONS_FILE_NAME = "permissions.toml";
17192
17361
  //#endregion
17193
17362
  //#region src/features/commands/codewhale-command.ts
17194
17363
  /**
@@ -20121,25 +20290,6 @@ var JunieCommand = class JunieCommand extends ToolCommand {
20121
20290
  }
20122
20291
  };
20123
20292
  //#endregion
20124
- //#region src/constants/kilo-paths.ts
20125
- const KILO_DIR = ".kilo";
20126
- const KILO_GLOBAL_DIR = (0, node_path.join)(".config", "kilo");
20127
- const KILO_RULES_DIR_NAME = "rules";
20128
- const KILO_COMMANDS_DIR_PATH = (0, node_path.join)(KILO_DIR, "commands");
20129
- const KILO_GLOBAL_COMMANDS_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "commands");
20130
- const KILO_SKILLS_DIR_PATH = (0, node_path.join)(KILO_DIR, "skills");
20131
- const KILO_AGENTS_DIR_PATH = (0, node_path.join)(KILO_DIR, "agents");
20132
- const KILO_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "agents");
20133
- const KILO_PLUGINS_DIR_PATH = (0, node_path.join)(KILO_DIR, "plugins");
20134
- const KILO_GLOBAL_PLUGINS_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "plugins");
20135
- const KILO_PLUGIN_DIR_PATH = (0, node_path.join)(KILO_DIR, "plugin");
20136
- const KILO_GLOBAL_PLUGIN_DIR_PATH = (0, node_path.join)(KILO_GLOBAL_DIR, "plugin");
20137
- const KILO_RULE_FILE_NAME = "AGENTS.md";
20138
- const KILO_IGNORE_FILE_NAME = ".kilocodeignore";
20139
- const KILO_JSONC_FILE_NAME = "kilo.jsonc";
20140
- const KILO_JSON_FILE_NAME = "kilo.json";
20141
- const KILO_HOOKS_FILE_NAME = "rulesync-hooks.js";
20142
- //#endregion
20143
20293
  //#region src/features/commands/kilo-command.ts
20144
20294
  const KiloCommandFrontmatterSchema = zod_mini.z.looseObject({
20145
20295
  description: zod_mini.z.optional(zod_mini.z.string()),
@@ -20633,9 +20783,26 @@ const OpenCodeCommandFrontmatterSchema = zod_mini.z.looseObject({
20633
20783
  description: zod_mini.z.optional(zod_mini.z.string()),
20634
20784
  agent: (0, zod_mini.optional)(zod_mini.z.string()),
20635
20785
  subtask: (0, zod_mini.optional)(zod_mini.z.boolean()),
20786
+ subagent: (0, zod_mini.optional)(zod_mini.z.boolean()),
20636
20787
  model: (0, zod_mini.optional)(zod_mini.z.string()),
20637
20788
  variant: (0, zod_mini.optional)(zod_mini.z.string())
20638
20789
  });
20790
+ /**
20791
+ * Rewrites a boolean `subagent` (OpenCode V2) into `subtask`, the key both V1
20792
+ * and V2 read: V1's command schema rejects unknown keys, while V2 resolves
20793
+ * `subagent ?? subtask`. `subagent` therefore wins when both are set, matching
20794
+ * V2. A non-boolean `subagent` is left untouched for validation to report.
20795
+ *
20796
+ * @see https://opencode.ai/v2/docs/commands/
20797
+ */
20798
+ function foldSubagentAlias(fields) {
20799
+ if (typeof fields.subagent !== "boolean") return fields;
20800
+ const { subagent, ...rest } = fields;
20801
+ return {
20802
+ ...rest,
20803
+ subtask: subagent
20804
+ };
20805
+ }
20639
20806
  var OpenCodeCommand = class extends ToolCommand {
20640
20807
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
20641
20808
  static layout = OPENCODE_LAYOUT;
@@ -20663,7 +20830,8 @@ var OpenCodeCommand = class extends ToolCommand {
20663
20830
  return this.frontmatter;
20664
20831
  }
20665
20832
  toRulesyncCommand() {
20666
- const { description, ...restFields } = this.frontmatter;
20833
+ const { description, ...toolFields } = this.frontmatter;
20834
+ const restFields = foldSubagentAlias(toolFields);
20667
20835
  const { toolTarget } = this.constructor.layout;
20668
20836
  const rulesyncFrontmatter = {
20669
20837
  targets: ["*"],
@@ -20683,7 +20851,7 @@ var OpenCodeCommand = class extends ToolCommand {
20683
20851
  }
20684
20852
  static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
20685
20853
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
20686
- const opencodeFields = rulesyncFrontmatter[this.layout.toolTarget] ?? {};
20854
+ const opencodeFields = foldSubagentAlias(rulesyncFrontmatter[this.layout.toolTarget] ?? {});
20687
20855
  const opencodeFrontmatter = {
20688
20856
  description: rulesyncFrontmatter.description,
20689
20857
  ...opencodeFields
@@ -20739,7 +20907,8 @@ var OpenCodeCommand = class extends ToolCommand {
20739
20907
  * Imports commands defined inline in `opencode.json` / `opencode.jsonc` under
20740
20908
  * the top-level `command` key (in addition to the Markdown files under
20741
20909
  * `.opencode/commands/`). Each entry's `template` becomes the command body,
20742
- * while `description` / `agent` / `model` / `subtask` map to the frontmatter.
20910
+ * while `description` / `agent` / `model` / `subtask` map to the frontmatter
20911
+ * (a V2 `subagent` is read as `subtask`, taking precedence like V2 does).
20743
20912
  *
20744
20913
  * Import-only: this is invoked by the commands processor when loading tool
20745
20914
  * files for conversion to rulesync, never for orphan deletion.
@@ -20759,11 +20928,12 @@ var OpenCodeCommand = class extends ToolCommand {
20759
20928
  const entry = asOpencodeEntries(rawEntry);
20760
20929
  if (!entry) continue;
20761
20930
  const body = typeof entry.template === "string" ? entry.template : "";
20931
+ const { subtask } = foldSubagentAlias(entry);
20762
20932
  const frontmatter = {
20763
20933
  ...typeof entry.description === "string" && { description: entry.description },
20764
20934
  ...typeof entry.agent === "string" && { agent: entry.agent },
20765
20935
  ...typeof entry.model === "string" && { model: entry.model },
20766
- ...typeof entry.subtask === "boolean" && { subtask: entry.subtask }
20936
+ ...typeof subtask === "boolean" && { subtask }
20767
20937
  };
20768
20938
  commands.push(new this({
20769
20939
  outputRoot,
@@ -20816,6 +20986,7 @@ const OMP_RULE_FILE_NAME = "AGENTS.md";
20816
20986
  const OMP_RULES_DIR_NAME = "rules";
20817
20987
  const OMP_AGENTS_DIR_NAME = "agents";
20818
20988
  const OMP_MCP_FILE_NAME = "mcp.json";
20989
+ const OMP_CONFIG_FILE_NAME = "config.yml";
20819
20990
  const OMP_LAYOUT = {
20820
20991
  toolTarget: "omp",
20821
20992
  dir: OMP_DIR,
@@ -22145,6 +22316,7 @@ function warpcliConfigDir() {
22145
22316
  }
22146
22317
  }
22147
22318
  const WARP_RULE_FILE_NAME = "AGENTS.md";
22319
+ const WARP_LEGACY_RULE_FILE_NAME = "WARP.md";
22148
22320
  const WARP_GLOBAL_RULE_DIR = ".agents";
22149
22321
  const WARP_MCP_FILE_NAME = ".mcp.json";
22150
22322
  const WARP_PERMISSIONS_FILE_NAME = "settings.toml";
@@ -35725,7 +35897,9 @@ var JunieIgnore = class JunieIgnore extends ToolIgnore {
35725
35897
  * KiloIgnore represents ignore patterns for the Kilo Code VSCode extension.
35726
35898
  *
35727
35899
  * Based on the Kilo Code specification:
35728
- * - File location: Workspace root folder only (.kilocodeignore)
35900
+ * - File location: workspace root (.kilocodeignore) for project scope, and
35901
+ * `~/.kilocode/.kilocodeignore` for global scope. Kilo merges the global
35902
+ * patterns first and the project ones after, so the project file wins.
35729
35903
  * - Syntax: Same as .gitignore
35730
35904
  * - Immediate reflection when saved
35731
35905
  * - Complete blocking of file access for ignored patterns
@@ -35733,11 +35907,12 @@ var JunieIgnore = class JunieIgnore extends ToolIgnore {
35733
35907
  *
35734
35908
  * Kilo reads `.kilocodeignore` (not `.kiloignore`), so emitting `.kiloignore`
35735
35909
  * left the file inert. https://kilo.ai/docs/customize/context/kilocodeignore
35910
+ * @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/ignore-migrator.ts
35736
35911
  */
35737
35912
  var KiloIgnore = class KiloIgnore extends ToolIgnore {
35738
- static getSettablePaths() {
35913
+ static getSettablePaths({ global = false } = {}) {
35739
35914
  return {
35740
- relativeDirPath: ".",
35915
+ relativeDirPath: global ? KILO_LEGACY_GLOBAL_DIR : ".",
35741
35916
  relativeFilePath: KILO_IGNORE_FILE_NAME
35742
35917
  };
35743
35918
  }
@@ -35750,35 +35925,40 @@ var KiloIgnore = class KiloIgnore extends ToolIgnore {
35750
35925
  /**
35751
35926
  * Create KiloIgnore from RulesyncIgnore
35752
35927
  */
35753
- static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
35928
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
35754
35929
  const body = rulesyncIgnore.getFileContent();
35930
+ const paths = this.getSettablePaths({ global });
35755
35931
  return new KiloIgnore({
35756
35932
  outputRoot,
35757
- relativeDirPath: this.getSettablePaths().relativeDirPath,
35758
- relativeFilePath: this.getSettablePaths().relativeFilePath,
35759
- fileContent: body
35933
+ relativeDirPath: paths.relativeDirPath,
35934
+ relativeFilePath: paths.relativeFilePath,
35935
+ fileContent: body,
35936
+ global
35760
35937
  });
35761
35938
  }
35762
35939
  /**
35763
35940
  * Load KiloIgnore from .kilocodeignore file
35764
35941
  */
35765
- static async fromFile({ outputRoot = process.cwd(), validate = true }) {
35766
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
35942
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
35943
+ const paths = this.getSettablePaths({ global });
35944
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
35767
35945
  return new KiloIgnore({
35768
35946
  outputRoot,
35769
- relativeDirPath: this.getSettablePaths().relativeDirPath,
35770
- relativeFilePath: this.getSettablePaths().relativeFilePath,
35947
+ relativeDirPath: paths.relativeDirPath,
35948
+ relativeFilePath: paths.relativeFilePath,
35771
35949
  fileContent,
35772
- validate
35950
+ validate,
35951
+ global
35773
35952
  });
35774
35953
  }
35775
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
35954
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
35776
35955
  return new KiloIgnore({
35777
35956
  outputRoot,
35778
35957
  relativeDirPath,
35779
35958
  relativeFilePath,
35780
35959
  fileContent: "",
35781
- validate: false
35960
+ validate: false,
35961
+ global
35782
35962
  });
35783
35963
  }
35784
35964
  };
@@ -36452,6 +36632,7 @@ const ignoreProcessorGlobalToolTargets = [
36452
36632
  "continue",
36453
36633
  "devin",
36454
36634
  "grokcli",
36635
+ "kilo",
36455
36636
  "kiro",
36456
36637
  "kiro-cli",
36457
36638
  "kiro-ide",
@@ -40313,6 +40494,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
40313
40494
  * `g` flag is required because callers may have multiple references per value.
40314
40495
  */
40315
40496
  const CANONICAL_ENV_VAR_PATTERN = /\$\{(?!env:)([^}:]+)\}/g;
40497
+ /**
40498
+ * OpenCode / Kilo `{env:VAR}` env var reference pattern. The negative lookbehind
40499
+ * avoids matching Cursor's `${env:VAR}` format.
40500
+ */
40501
+ const BRACE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
40316
40502
  function convertRecordValues({ record, pattern, replacement }) {
40317
40503
  return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, v.replace(pattern, replacement)]));
40318
40504
  }
@@ -40364,6 +40550,14 @@ function convertEnvVarRefsToToolFormat({ mcpServers, replacement }) {
40364
40550
  }) }
40365
40551
  }]));
40366
40552
  }
40553
+ /**
40554
+ * Names of the servers whose `env` or `headers` values carry at least one
40555
+ * canonical `${VAR}` env var reference.
40556
+ */
40557
+ function findServersWithEnvVarRefs(mcpServers) {
40558
+ const hasRef = (record) => Object.values(record ?? {}).some((value) => value.search(CANONICAL_ENV_VAR_PATTERN) !== -1);
40559
+ return Object.entries(mcpServers).filter(([, config]) => hasRef(config.env) || hasRef(config.headers)).map(([name]) => name);
40560
+ }
40367
40561
  //#endregion
40368
40562
  //#region src/features/mcp/cursor-mcp.ts
40369
40563
  const CURSOR_ENV_VAR_PATTERN = /\$\{env:([^}:]+)\}/g;
@@ -42652,17 +42846,50 @@ function readExistingKiloMcpEntries(fileContent) {
42652
42846
  }
42653
42847
  return entries;
42654
42848
  }
42655
- function convertToKiloFormat(mcpServers, existingMcp, logger) {
42849
+ const KILO_UNTRUSTED_ENV_REF_PATTERN = /\{env:[^}]+\}/;
42850
+ /**
42851
+ * Judged on the entry as written, not the canonical server: fields the entry
42852
+ * drops (a toggle keeps only `enabled`) cannot make Kilo reject the file.
42853
+ */
42854
+ function rejectUntrustedEnvRef(serverName, converted, rejectEnvRefs, logger) {
42855
+ if (!rejectEnvRefs || converted === null) return converted;
42856
+ if (!KILO_UNTRUSTED_ENV_REF_PATTERN.test(JSON.stringify(converted))) return converted;
42857
+ return warnAndSkipMcpServer({
42858
+ toolName: "Kilo",
42859
+ serverName,
42860
+ reason: "an {env:...} reference, which makes Kilo reject the whole project config",
42861
+ logger
42862
+ });
42863
+ }
42864
+ function convertToKiloFormat(mcpServers, existingMcp, logger, rejectEnvRefs = false) {
42656
42865
  const tools = {};
42657
42866
  return {
42658
42867
  mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
42659
- const converted = convertServerToKiloFormat(serverName, serverConfig, Object.hasOwn(existingMcp, serverName) ? existingMcp[serverName] : void 0, logger);
42868
+ const converted = rejectUntrustedEnvRef(serverName, convertServerToKiloFormat(serverName, serverConfig, Object.hasOwn(existingMcp, serverName) ? existingMcp[serverName] : void 0, logger), rejectEnvRefs, logger);
42660
42869
  collectKiloServerTools(tools, serverName, serverConfig);
42661
42870
  return converted === null ? null : [serverName, converted];
42662
42871
  }).filter((entry) => entry !== null)),
42663
42872
  tools
42664
42873
  };
42665
42874
  }
42875
+ /**
42876
+ * Kilo expands only `{env:VAR}`, never the canonical `${VAR}`, and only in a
42877
+ * trusted config: the global `~/.config/kilo/kilo.jsonc`. A project config is
42878
+ * untrusted, so any `{env:` in it makes Kilo drop the whole file (or, in MCP
42879
+ * headers, the server). In project scope canonical references are therefore
42880
+ * left as written — Kilo passes them through literally — with a warning (a
42881
+ * written entry already carrying `{env:` is skipped by `convertToKiloFormat`).
42882
+ * @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/config/variable.ts
42883
+ */
42884
+ function resolveKiloEnvVarRefs({ mcpServers, global, logger }) {
42885
+ if (global) return convertEnvVarRefsToToolFormat({
42886
+ mcpServers,
42887
+ replacement: "{env:$1}"
42888
+ });
42889
+ const serverNames = findServersWithEnvVarRefs(mcpServers);
42890
+ 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.`);
42891
+ return mcpServers;
42892
+ }
42666
42893
  var KiloMcp = class KiloMcp extends ToolMcp {
42667
42894
  json;
42668
42895
  constructor(params) {
@@ -42761,7 +42988,11 @@ var KiloMcp = class KiloMcp extends ToolMcp {
42761
42988
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
42762
42989
  }
42763
42990
  const existingMcp = readExistingKiloMcpEntries(fileContent);
42764
- const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(rulesyncMcp.getMcpServers(), existingMcp, logger);
42991
+ const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(resolveKiloEnvVarRefs({
42992
+ mcpServers: rulesyncMcp.getMcpServers(),
42993
+ global,
42994
+ logger
42995
+ }), existingMcp, logger, !global);
42765
42996
  return new KiloMcp({
42766
42997
  outputRoot,
42767
42998
  relativeDirPath: basePaths.relativeDirPath,
@@ -42827,7 +43058,10 @@ var KiloMcp = class KiloMcp extends ToolMcp {
42827
43058
  });
42828
43059
  }
42829
43060
  toRulesyncMcp() {
42830
- const { shared, toolOnly } = splitMcpServersByTransport(convertFromKiloFormat(this.json.mcp ?? {}, this.json.tools));
43061
+ const { shared, toolOnly } = splitMcpServersByTransport(convertEnvVarRefsFromToolFormat({
43062
+ mcpServers: convertFromKiloFormat(this.json.mcp ?? {}, this.json.tools),
43063
+ pattern: BRACE_ENV_VAR_PATTERN
43064
+ }));
42831
43065
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
42832
43066
  mcpServers: shared,
42833
43067
  ...Object.keys(toolOnly).length > 0 && { kilo: { mcpServers: toolOnly } }
@@ -43284,7 +43518,6 @@ var KiroMcp = class KiroMcp extends ToolMcp {
43284
43518
  };
43285
43519
  //#endregion
43286
43520
  //#region src/features/mcp/opencode-mcp.ts
43287
- const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
43288
43521
  const OpencodeMcpLocalServerSchema = zod_mini.z.looseObject({
43289
43522
  type: zod_mini.z.literal("local"),
43290
43523
  command: zod_mini.z.array(zod_mini.z.string()),
@@ -43667,7 +43900,7 @@ var OpencodeMcp = class extends ToolMcp {
43667
43900
  toRulesyncMcp() {
43668
43901
  const { shared, toolOnly } = splitMcpServersByTransport(convertEnvVarRefsFromToolFormat({
43669
43902
  mcpServers: convertFromOpencodeFormat(this.json.mcp ?? {}, this.json.tools),
43670
- pattern: OPENCODE_ENV_VAR_PATTERN
43903
+ pattern: BRACE_ENV_VAR_PATTERN
43671
43904
  }));
43672
43905
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
43673
43906
  mcpServers: shared,
@@ -44950,6 +45183,28 @@ const REASONIX_RETIRED_PLUGIN_FIELDS = ["trusted_read_only_tools"];
44950
45183
  function warnAboutRetiredFields({ name, serverRecord, logger }) {
44951
45184
  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.`);
44952
45185
  }
45186
+ /**
45187
+ * The `concurrency` values Reasonix honors. It trims and lowercases before
45188
+ * comparing (`mcpServerIsSerial` in `internal/agent/mcp_concurrency.go`), so
45189
+ * `" Serial "` counts too and is written as authored.
45190
+ */
45191
+ const REASONIX_CONCURRENCY_VALUES = /* @__PURE__ */ new Set(["serial", "parallel"]);
45192
+ /**
45193
+ * Why a `concurrency` / `auto_start` value cannot be written, or `undefined`
45194
+ * when it can (and for every other field). A wrong type is the case that
45195
+ * matters: Reasonix declares the fields as `string` / `*bool`, and a TOML type
45196
+ * mismatch makes it refuse the whole config file. An unknown `concurrency`
45197
+ * string decodes fine but is ignored in favor of the server-name default, so it
45198
+ * is dropped too rather than written as a setting that does nothing.
45199
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/config/plugin_entry.go
45200
+ */
45201
+ function invalidSchedulingFieldReason(field, value) {
45202
+ if (field === "auto_start" && typeof value !== "boolean") return "Reasonix expects a boolean and fails to load a config file holding any other type.";
45203
+ if (field === "concurrency") {
45204
+ if (typeof value !== "string") return "Reasonix expects a string and fails to load a config file holding any other type.";
45205
+ 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.`;
45206
+ }
45207
+ }
44953
45208
  function rulesyncMcpServerToReasonix(name, server, logger) {
44954
45209
  const serverRecord = server;
44955
45210
  const type = resolveReasonixType(server);
@@ -44974,7 +45229,14 @@ function rulesyncMcpServerToReasonix(name, server, logger) {
44974
45229
  }
44975
45230
  for (const field of REASONIX_PLUGIN_FIELDS) {
44976
45231
  if (field === "type" || field === "command" || field === "args") continue;
44977
- if (serverRecord[field] !== void 0) plugin[field] = serverRecord[field];
45232
+ const value = serverRecord[field];
45233
+ if (value === void 0) continue;
45234
+ const invalidReason = invalidSchedulingFieldReason(field, value);
45235
+ if (invalidReason !== void 0) {
45236
+ logger?.warn(`Reasonix MCP: dropping "${field}" from "${name}"; ${invalidReason}`);
45237
+ continue;
45238
+ }
45239
+ plugin[field] = value;
44978
45240
  }
44979
45241
  warnAboutRetiredFields({
44980
45242
  name,
@@ -52041,7 +52303,7 @@ const CODEBUDDY_RULE_LISTS = [
52041
52303
  "ask",
52042
52304
  "deny"
52043
52305
  ];
52044
- const CATCH_ALL_PATTERN$10 = "*";
52306
+ const CATCH_ALL_PATTERN$11 = "*";
52045
52307
  const MCP_CATEGORY = "mcp";
52046
52308
  const MCP_RULE_PREFIX = "mcp__";
52047
52309
  const ALL_MCP_RULE = "mcp__*";
@@ -52114,7 +52376,7 @@ function categoryOfRule$2(rule) {
52114
52376
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
52115
52377
  */
52116
52378
  function buildCodebuddyRule({ category, pattern, action, logger }) {
52117
- const catchAll = pattern === CATCH_ALL_PATTERN$10;
52379
+ const catchAll = pattern === CATCH_ALL_PATTERN$11;
52118
52380
  if (category === MCP_CATEGORY) {
52119
52381
  if (catchAll && action === "allow") {
52120
52382
  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').`);
@@ -52123,7 +52385,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
52123
52385
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
52124
52386
  }
52125
52387
  if (category === "*") {
52126
- if (catchAll) return CATCH_ALL_PATTERN$10;
52388
+ if (catchAll) return CATCH_ALL_PATTERN$11;
52127
52389
  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').`);
52128
52390
  return null;
52129
52391
  }
@@ -52141,7 +52403,7 @@ function parseCodebuddyRule(rule) {
52141
52403
  const { specifier } = splitCodebuddyRule(rule);
52142
52404
  if (specifier === void 0) return {
52143
52405
  category,
52144
- pattern: CATCH_ALL_PATTERN$10
52406
+ pattern: CATCH_ALL_PATTERN$11
52145
52407
  };
52146
52408
  const trimmed = specifier.trim();
52147
52409
  return trimmed === "" ? null : {
@@ -52317,6 +52579,633 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
52317
52579
  }
52318
52580
  }
52319
52581
  //#endregion
52582
+ //#region src/features/permissions/codewhale-permissions.ts
52583
+ const CODEWHALE_SHELL_TOOL = "exec_shell";
52584
+ /**
52585
+ * Canonical path categories and the Codewhale policy tool names their rules are
52586
+ * written for. Codewhale consults typed rules only for `exec_shell` and these
52587
+ * file tools, so a rule for any other tool (`fetch_url`, `web_search`, an MCP
52588
+ * tool, ...) is never evaluated. `edit` covers `apply_patch` too: both modify
52589
+ * existing files, and an `edit` deny that left `apply_patch` open would not
52590
+ * hold.
52591
+ *
52592
+ * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/core/engine.rs (`file_tool_permission_paths`)
52593
+ * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/tools/canonical_action.rs
52594
+ */
52595
+ const CODEWHALE_PATH_CATEGORY_TOOLS = {
52596
+ read: ["read_file"],
52597
+ write: ["write_file"],
52598
+ edit: ["edit_file", "apply_patch"],
52599
+ grep: ["grep_files"],
52600
+ glob: ["file_search"],
52601
+ list: ["list_dir"]
52602
+ };
52603
+ /**
52604
+ * `apply_patch` can also create files (a `--- /dev/null` hunk), so a `write`
52605
+ * restriction is written for it too; a `write` allow is not, since it would
52606
+ * approve edits to existing files the `edit` category never allowed.
52607
+ *
52608
+ * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/tools/apply_patch.rs
52609
+ */
52610
+ const CODEWHALE_WRITE_RESTRICTION_TOOLS = ["write_file", "apply_patch"];
52611
+ /** The Codewhale tools a canonical path rule is written for. */
52612
+ function pathCategoryTools(category, action) {
52613
+ if (category === "write" && action !== "allow") return CODEWHALE_WRITE_RESTRICTION_TOOLS;
52614
+ return CODEWHALE_PATH_CATEGORY_TOOLS[category] ?? [];
52615
+ }
52616
+ /** The canonical path categories a Codewhale file tool's rule can come from. */
52617
+ const CODEWHALE_TOOL_TO_PATH_CATEGORIES = {
52618
+ read_file: ["read"],
52619
+ write_file: ["write"],
52620
+ edit_file: ["edit"],
52621
+ apply_patch: ["edit", "write"],
52622
+ grep_files: ["grep"],
52623
+ file_search: ["glob"],
52624
+ list_dir: ["list"]
52625
+ };
52626
+ const CODEWHALE_GLOBAL_ONLY_MESSAGE = "Codewhale permissions are global-only; use --global to sync ~/.codewhale/permissions.toml";
52627
+ const COMMAND_WILDCARD_CHARACTERS = /[*?[]/;
52628
+ const PATH_GLOB_CHARACTERS = /[*?[\]{}]/;
52629
+ function isCatchAllPattern(pattern) {
52630
+ return pattern === "*" || pattern === "**";
52631
+ }
52632
+ /**
52633
+ * The literal command prefix a wildcard pattern pins down, cut back to a word
52634
+ * boundary so it never names a partial word (`git push* --force` pins down
52635
+ * `git`, not `git push`). Empty when the pattern starts with a wildcard.
52636
+ */
52637
+ function literalCommandPrefix(pattern) {
52638
+ const firstWildcard = pattern.search(COMMAND_WILDCARD_CHARACTERS);
52639
+ const head = pattern.slice(0, firstWildcard);
52640
+ if (/\s$/.test(head)) return head.trim();
52641
+ const lastBoundary = head.search(/\s\S*$/);
52642
+ return lastBoundary < 0 ? "" : head.slice(0, lastBoundary).trim();
52643
+ }
52644
+ /**
52645
+ * Convert one canonical `bash` rule. Codewhale matches `command` as a
52646
+ * word-boundary prefix of the invocation (or the whole invocation with
52647
+ * `command_exact = true`); it expands no wildcards.
52648
+ *
52649
+ * - `*` / `**` → a tool-wide rule.
52650
+ * - `git *` → the prefix `git`.
52651
+ * - A wildcard-free `allow` → an exact command, so it approves nothing more
52652
+ * than the command it names. A wildcard-free `deny` / `ask` → a prefix, the
52653
+ * way Codewhale's own deny rules are matched, so the restriction also covers
52654
+ * the command with more arguments.
52655
+ * - Any other wildcard: a restricting rule is broadened to the literal prefix
52656
+ * it pins down; an `allow` is skipped, since no prefix approves only what it
52657
+ * names.
52658
+ */
52659
+ function convertShellRule({ pattern, action, source = SHELL_PERMISSION_CATEGORY, logger }) {
52660
+ const trimmed = pattern.trim();
52661
+ if (isCatchAllPattern(trimmed)) return {
52662
+ tool: CODEWHALE_SHELL_TOOL,
52663
+ action
52664
+ };
52665
+ if (trimmed === "") {
52666
+ logger?.warn(`Codewhale permissions: skipping empty ${source} ${action} pattern.`);
52667
+ return null;
52668
+ }
52669
+ if (!COMMAND_WILDCARD_CHARACTERS.test(trimmed)) return action === "allow" ? {
52670
+ tool: CODEWHALE_SHELL_TOOL,
52671
+ command: trimmed,
52672
+ command_exact: true,
52673
+ action
52674
+ } : {
52675
+ tool: CODEWHALE_SHELL_TOOL,
52676
+ command: trimmed,
52677
+ action
52678
+ };
52679
+ const trailingPrefix = trimmed.endsWith(" *") ? trimmed.slice(0, -2).trim() : null;
52680
+ if (trailingPrefix && !COMMAND_WILDCARD_CHARACTERS.test(trailingPrefix)) return {
52681
+ tool: CODEWHALE_SHELL_TOOL,
52682
+ command: trailingPrefix,
52683
+ action
52684
+ };
52685
+ if (action === "allow") {
52686
+ logger?.warn(`Codewhale permissions: skipping ${source} allow "${pattern}" because Codewhale matches commands by prefix and expands no wildcards.`);
52687
+ return null;
52688
+ }
52689
+ const prefix = literalCommandPrefix(trimmed);
52690
+ if (prefix === "") {
52691
+ logger?.warn(`Codewhale permissions: skipping ${source} ${action} "${pattern}" because it pins down no literal command prefix Codewhale could match.`);
52692
+ return null;
52693
+ }
52694
+ logger?.warn(`Codewhale permissions: broadening ${source} ${action} "${pattern}" to the command prefix "${prefix}" because Codewhale expands no wildcards.`);
52695
+ return {
52696
+ tool: CODEWHALE_SHELL_TOOL,
52697
+ command: prefix,
52698
+ action
52699
+ };
52700
+ }
52701
+ /**
52702
+ * Convert one canonical path rule. Codewhale matches `path` exactly (after
52703
+ * normalizing it against the workspace) and expands no globs, so `*` / `**`
52704
+ * become a tool-wide rule and any other glob is skipped: narrowing it to a
52705
+ * single path would leave the rest of the glob unrestricted, and widening it to
52706
+ * the whole tool would restrict far more than written.
52707
+ */
52708
+ function convertPathRules({ category, pattern, action, logger }) {
52709
+ const tools = pathCategoryTools(category, action);
52710
+ const trimmed = pattern.trim();
52711
+ if (isCatchAllPattern(trimmed)) return tools.map((tool) => ({
52712
+ tool,
52713
+ action
52714
+ }));
52715
+ if (trimmed === "" || PATH_GLOB_CHARACTERS.test(trimmed)) {
52716
+ logger?.warn(`Codewhale permissions: skipping ${category} ${action} "${pattern}" because Codewhale matches exact paths and expands no globs.`);
52717
+ return [];
52718
+ }
52719
+ return tools.map((tool) => ({
52720
+ tool,
52721
+ path: trimmed,
52722
+ action
52723
+ }));
52724
+ }
52725
+ function ruleKey(rule) {
52726
+ return JSON.stringify([
52727
+ rule.tool,
52728
+ rule.command ?? null,
52729
+ rule.command_exact ?? false,
52730
+ rule.path ?? null,
52731
+ rule.workspace ?? null,
52732
+ rule.action ?? "ask"
52733
+ ]);
52734
+ }
52735
+ function withExplicitAction(rule) {
52736
+ return {
52737
+ ...rule,
52738
+ action: rule.action ?? "ask"
52739
+ };
52740
+ }
52741
+ /**
52742
+ * Codewhale picks the matching rule by action (`deny` > `ask` > `allow`) before
52743
+ * specificity, so file order never changes a decision. The rules are still
52744
+ * sorted that way, then by tool, so the output is stable and reads in the order
52745
+ * Codewhale applies it.
52746
+ */
52747
+ function sortRules(rules) {
52748
+ return rules.map((rule, index) => ({
52749
+ rule,
52750
+ index
52751
+ })).toSorted((left, right) => {
52752
+ const actionOrder = PERMISSION_ACTION_PRIORITY[right.rule.action] - PERMISSION_ACTION_PRIORITY[left.rule.action];
52753
+ if (actionOrder !== 0) return actionOrder;
52754
+ const toolOrder = left.rule.tool.localeCompare(right.rule.tool);
52755
+ if (toolOrder !== 0) return toolOrder;
52756
+ return left.index - right.index;
52757
+ }).map(({ rule }) => rule);
52758
+ }
52759
+ function recordDroppedRestriction({ dropped, tools, glob }) {
52760
+ for (const tool of tools) dropped.set(tool, [...dropped.get(tool) ?? [], glob]);
52761
+ }
52762
+ /**
52763
+ * A `[...]` class in a restriction widens to `?` (any one character): matched
52764
+ * after lowercasing, a negated or mixed-case class would otherwise narrow what
52765
+ * the restriction covers.
52766
+ */
52767
+ function widenRestrictionClasses(pattern) {
52768
+ return pattern.replace(/\[[!^]?\]?[^\]]*\]/g, "?");
52769
+ }
52770
+ /**
52771
+ * An `allow` rule names a literal command or path, so a `[` in it widens to `?`
52772
+ * rather than opening a class that would not even match the rule itself.
52773
+ */
52774
+ function literalAsGlob(value) {
52775
+ return value.replaceAll("[", "?");
52776
+ }
52777
+ /**
52778
+ * Codewhale matches commands case-insensitively and word by word, so commands
52779
+ * are compared lowercased with their whitespace collapsed.
52780
+ */
52781
+ function normalizeCommandGlob(glob) {
52782
+ return glob.trim().replace(/\s+/g, " ").toLowerCase();
52783
+ }
52784
+ /**
52785
+ * Codewhale normalizes a rule's path against the workspace, dropping empty and
52786
+ * `.` segments, so the same file can be spelled several ways. Paths are
52787
+ * compared lowercased (for case-insensitive file systems) with those segments
52788
+ * dropped, a `{a,b}` group widens to `*`, a `**` segment widens so it also
52789
+ * matches no directory at all (a leading `**` segment before `*.pem` reaches
52790
+ * `dev.pem`, and `secrets/**` reaches `secrets`), and a path that is absolute,
52791
+ * home-relative or climbs with `..` widens to `*` outright, since it cannot be
52792
+ * compared against a workspace-relative one. Every rewrite only widens a
52793
+ * pattern, so an inexact comparison withholds an `allow` rather than writing
52794
+ * one a dropped restriction overlaps.
52795
+ */
52796
+ function normalizePathGlob(glob) {
52797
+ const normalized = glob.trim().replaceAll("\\", "/").toLowerCase();
52798
+ if (/^(?:\/|~|[a-z]:)/.test(normalized) || /(?:^|\/)\.\.(?:\/|$)/.test(normalized)) return "*";
52799
+ return normalized.split("/").filter((segment) => segment !== "" && segment !== ".").join("/").replace(/\{.*\}/g, "*").replace(/\*\*\//g, "*").replace(/\/\*\*$/, "*");
52800
+ }
52801
+ /** A dropped `bash` (or all-tools) restriction, as a glob to compare `allow` rules against. */
52802
+ function shellRestrictionGlob(pattern) {
52803
+ return normalizeCommandGlob(widenRestrictionClasses(pattern));
52804
+ }
52805
+ /** A dropped path restriction, as a glob to compare `allow` rules against. */
52806
+ function pathRestrictionGlob(pattern) {
52807
+ return normalizePathGlob(widenRestrictionClasses(pattern));
52808
+ }
52809
+ /**
52810
+ * The file tools whose `path` is the directory a search or listing starts from,
52811
+ * so an `allow` on a directory reaches everything under it.
52812
+ */
52813
+ const CODEWHALE_DIRECTORY_ROOTED_TOOLS = /* @__PURE__ */ new Set([
52814
+ "grep_files",
52815
+ "file_search",
52816
+ "list_dir"
52817
+ ]);
52818
+ /**
52819
+ * The globs a written `allow` rule approves, for comparison with dropped
52820
+ * restrictions. A path that normalizes to the workspace root (`.`) approves
52821
+ * everything, and a directory-rooted tool's path approves what lies under it.
52822
+ */
52823
+ function allowRuleComparisonGlobs(rule) {
52824
+ if (rule.tool === CODEWHALE_SHELL_TOOL) {
52825
+ if (rule.command === void 0) return ["*"];
52826
+ const command = normalizeCommandGlob(literalAsGlob(rule.command));
52827
+ return rule.command_exact ? [command] : [command, `${command} *`];
52828
+ }
52829
+ const path = rule.path === void 0 ? "" : normalizePathGlob(literalAsGlob(rule.path));
52830
+ if (path === "") return ["*"];
52831
+ return CODEWHALE_DIRECTORY_ROOTED_TOOLS.has(rule.tool) ? [path, `${path}/*`] : [path];
52832
+ }
52833
+ /**
52834
+ * Drop every `allow` rule that a restriction which could not be written
52835
+ * overlaps (any one command or path matches both), so what the restriction
52836
+ * named reaches Codewhale's approval prompt instead of being auto-approved. An
52837
+ * `allow` no dropped restriction overlaps is kept. Comparisons share one budget;
52838
+ * once it runs out every comparison answers "overlaps", so the rule is withheld.
52839
+ */
52840
+ function withholdShadowedAllowRules({ rules, dropped, logger }) {
52841
+ const budget = createIntersectionBudget();
52842
+ const tests = new Map([...dropped].map(([tool, globs]) => [tool, createShadowingRestrictionsTest(globs.map((pattern) => ({
52843
+ pattern,
52844
+ fromAllToolsCategory: false
52845
+ })), { budget })]));
52846
+ return rules.filter((rule) => {
52847
+ const test = tests.get(rule.tool);
52848
+ if ((rule.action ?? "ask") !== "allow" || test === void 0) return true;
52849
+ const shadowing = [...new Set(allowRuleComparisonGlobs(rule).flatMap((glob) => test(glob)))];
52850
+ if (shadowing.length === 0) return true;
52851
+ 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.`);
52852
+ return false;
52853
+ });
52854
+ }
52855
+ /**
52856
+ * An all-tools `deny` / `ask` names paths as much as commands (`secrets/**`
52857
+ * under `*` denies a path), so it is written for every file tool as well. A
52858
+ * pattern Codewhale cannot match as a path is recorded as dropped for every file
52859
+ * tool instead, the same way a skipped restriction of a path category is.
52860
+ */
52861
+ function convertAllToolsPathRestriction({ pattern, action, converted, dropped, logger }) {
52862
+ const categories = Object.keys(CODEWHALE_PATH_CATEGORY_TOOLS);
52863
+ const pathRules = categories.flatMap((category) => convertPathRules({
52864
+ category,
52865
+ pattern,
52866
+ action
52867
+ }));
52868
+ if (pathRules.length > 0) {
52869
+ converted.push(...pathRules);
52870
+ return;
52871
+ }
52872
+ logger?.warn(`Codewhale permissions: all-tools ${action} "${pattern}" cannot be written for Codewhale's file tools, which match exact paths and expand no globs.`);
52873
+ for (const category of categories) recordDroppedRestriction({
52874
+ dropped,
52875
+ tools: pathCategoryTools(category, action),
52876
+ glob: pathRestrictionGlob(pattern)
52877
+ });
52878
+ }
52879
+ /**
52880
+ * Convert the rules that govern shell commands (`bash`, plus the restricting
52881
+ * rules of the all-tools `*` category) into `converted`, recording in
52882
+ * `dropped` each restriction that could not be written.
52883
+ */
52884
+ function convertShellCommandRules({ config, converted, dropped, logger }) {
52885
+ const { rules: shellRules, ignoredAllToolsAllowPatterns } = collectShellCommandRules(config.permission);
52886
+ for (const { pattern, action, fromAllToolsCategory } of shellRules) {
52887
+ const rule = convertShellRule({
52888
+ pattern,
52889
+ action,
52890
+ source: fromAllToolsCategory ? "all-tools (as a shell command)" : SHELL_PERMISSION_CATEGORY,
52891
+ logger
52892
+ });
52893
+ if (rule) converted.push(rule);
52894
+ else if (action !== "allow") recordDroppedRestriction({
52895
+ dropped,
52896
+ tools: [CODEWHALE_SHELL_TOOL],
52897
+ glob: shellRestrictionGlob(pattern)
52898
+ });
52899
+ if (fromAllToolsCategory) convertAllToolsPathRestriction({
52900
+ pattern,
52901
+ action,
52902
+ converted,
52903
+ dropped,
52904
+ logger
52905
+ });
52906
+ }
52907
+ 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.`);
52908
+ }
52909
+ /**
52910
+ * Convert the canonical block. The all-tools `*` category contributes its
52911
+ * restricting rules to shell commands (as every command-only adapter reads it,
52912
+ * see `collectShellCommandRules`), and its catch-all `*` restriction to every
52913
+ * file tool as well.
52914
+ *
52915
+ * A `deny` / `ask` Codewhale cannot express is not merely skipped: every
52916
+ * `allow` rule it overlaps for a tool it was meant to restrict is withheld too —
52917
+ * the generated ones and the preserved workspace-scoped grants alike — so the
52918
+ * commands or paths it named reach Codewhale's approval prompt instead of being
52919
+ * auto-approved by a broader `allow` (`{ "*": "allow", "secrets/**": "deny" }`
52920
+ * must not become a bare tool-wide allow). The `codewhale.rules` passthrough is
52921
+ * written as it stands.
52922
+ */
52923
+ function canonicalToCodewhaleRules({ config, preservedRules, logger }) {
52924
+ const converted = [];
52925
+ const dropped = /* @__PURE__ */ new Map();
52926
+ convertShellCommandRules({
52927
+ config,
52928
+ converted,
52929
+ dropped,
52930
+ logger
52931
+ });
52932
+ for (const [category, rules] of Object.entries(config.permission)) {
52933
+ if (category === "bash" || category === "*") continue;
52934
+ if (CODEWHALE_PATH_CATEGORY_TOOLS[category] === void 0) {
52935
+ 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.`);
52936
+ continue;
52937
+ }
52938
+ for (const [pattern, action] of Object.entries(rules)) {
52939
+ const pathRules = convertPathRules({
52940
+ category,
52941
+ pattern,
52942
+ action,
52943
+ logger
52944
+ });
52945
+ converted.push(...pathRules);
52946
+ if (pathRules.length === 0 && action !== "allow") recordDroppedRestriction({
52947
+ dropped,
52948
+ tools: pathCategoryTools(category, action),
52949
+ glob: pathRestrictionGlob(pattern)
52950
+ });
52951
+ }
52952
+ }
52953
+ const kept = withholdShadowedAllowRules({
52954
+ rules: converted,
52955
+ dropped,
52956
+ logger
52957
+ });
52958
+ const keptPreserved = withholdShadowedAllowRules({
52959
+ rules: preservedRules,
52960
+ dropped,
52961
+ logger
52962
+ });
52963
+ return [
52964
+ ...(config.codewhale?.rules ?? []).map(withExplicitAction),
52965
+ ...sortRules(kept),
52966
+ ...keptPreserved
52967
+ ];
52968
+ }
52969
+ function parsePermissionsDocument({ fileContent, filePath }) {
52970
+ if (fileContent.trim() === "") return {};
52971
+ try {
52972
+ return smol_toml.parse(fileContent);
52973
+ } catch (error) {
52974
+ throw new Error(`Failed to parse existing Codewhale permissions at ${filePath}: ${formatError(error)}`, { cause: error });
52975
+ }
52976
+ }
52977
+ /**
52978
+ * The workspace-scoped rules already in the file. Codewhale's approval card
52979
+ * appends one ("Always allow this exact rule in this repo") with the absolute
52980
+ * `workspace` it was granted in, and rulesync never derives a `workspace` from
52981
+ * the canonical block, so these are remembered grants rather than earlier
52982
+ * rulesync output and survive a regenerate. A record Codewhale would reject is
52983
+ * not carried over, since it would make Codewhale refuse the whole file.
52984
+ */
52985
+ function collectWorkspaceScopedRules({ document, logger }) {
52986
+ const { valid, invalidCount } = partitionRuleRecords(document);
52987
+ if (invalidCount > 0) logger?.warn(`Codewhale permissions: dropping ${invalidCount} existing rule record(s) Codewhale would reject (an unknown key or a wrong type).`);
52988
+ return valid.filter((rule) => rule.workspace !== void 0);
52989
+ }
52990
+ /**
52991
+ * Codewhale reads `permissions.toml` next to the `config.toml` in use, which
52992
+ * `CODEWHALE_HOME` or `CODEWHALE_CONFIG_PATH` can move away from
52993
+ * `~/.codewhale/`. rulesync always writes `~/.codewhale/permissions.toml`, so
52994
+ * say so when either is set rather than let the rules go unread.
52995
+ */
52996
+ function warnAboutRelocatedConfig(logger) {
52997
+ 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.`);
52998
+ }
52999
+ /** The records of a parsed file that Codewhale itself would accept. */
53000
+ function partitionRuleRecords(document) {
53001
+ const rawRules = Array.isArray(document.rules) ? document.rules : [];
53002
+ const valid = [];
53003
+ let invalidCount = 0;
53004
+ for (const raw of rawRules) {
53005
+ const parsed = CodewhalePermissionRuleSchema.safeParse(raw);
53006
+ if (parsed.success) valid.push(parsed.data);
53007
+ else invalidCount += 1;
53008
+ }
53009
+ return {
53010
+ valid,
53011
+ invalidCount
53012
+ };
53013
+ }
53014
+ /**
53015
+ * The canonical `bash` pattern a native shell rule round-trips through, or
53016
+ * `null` when the next generate would not write the same rule back from it.
53017
+ */
53018
+ function shellRuleToPattern(rule) {
53019
+ if (rule.path !== void 0) return null;
53020
+ if (rule.command === void 0) return rule.command_exact ? null : "*";
53021
+ const command = rule.command.trim();
53022
+ if (command === "" || COMMAND_WILDCARD_CHARACTERS.test(command)) return null;
53023
+ if (rule.command_exact) return rule.action === "allow" ? command : null;
53024
+ return `${command} *`;
53025
+ }
53026
+ function pathRuleToPattern(rule) {
53027
+ if (rule.command !== void 0 || rule.command_exact) return null;
53028
+ if (rule.path === void 0) return "*";
53029
+ const path = rule.path.trim();
53030
+ return path === "" || PATH_GLOB_CHARACTERS.test(path) ? null : path;
53031
+ }
53032
+ function toImportCandidates(rule) {
53033
+ if (rule.workspace !== void 0) return [];
53034
+ if (rule.tool === CODEWHALE_SHELL_TOOL) {
53035
+ const pattern = shellRuleToPattern(rule);
53036
+ return pattern === null ? [] : [{
53037
+ category: SHELL_PERMISSION_CATEGORY,
53038
+ pattern,
53039
+ rule
53040
+ }];
53041
+ }
53042
+ const pattern = pathRuleToPattern(rule);
53043
+ if (pattern === null) return [];
53044
+ return (CODEWHALE_TOOL_TO_PATH_CATEGORIES[rule.tool] ?? []).map((category) => ({
53045
+ category,
53046
+ pattern,
53047
+ rule
53048
+ }));
53049
+ }
53050
+ /**
53051
+ * Split the native rules into the canonical block and the `codewhale.rules`
53052
+ * passthrough so that the next generate writes the same file back. A canonical
53053
+ * pattern is taken only when every tool its category expands to carries the
53054
+ * same rule (`edit` needs both `edit_file` and `apply_patch`, a `write`
53055
+ * restriction both `write_file` and `apply_patch`). A rule no canonical pattern
53056
+ * claims — including the weaker of two rules for one pattern — stays native.
53057
+ */
53058
+ function codewhaleRulesToCanonical(rules) {
53059
+ const permission = {};
53060
+ const claimedIndexes = /* @__PURE__ */ new Set();
53061
+ const grouped = /* @__PURE__ */ new Map();
53062
+ for (const [index, rule] of rules.entries()) for (const candidate of toImportCandidates(rule)) {
53063
+ const key = JSON.stringify([
53064
+ candidate.category,
53065
+ candidate.pattern,
53066
+ rule.action
53067
+ ]);
53068
+ grouped.set(key, [...grouped.get(key) ?? [], {
53069
+ ...candidate,
53070
+ index
53071
+ }]);
53072
+ }
53073
+ const strongestFirst = [...grouped.values()].toSorted((left, right) => PERMISSION_ACTION_PRIORITY[right[0].rule.action] - PERMISSION_ACTION_PRIORITY[left[0].rule.action]);
53074
+ for (const group of strongestFirst) {
53075
+ const { category, pattern, rule } = group[0];
53076
+ const expectedTools = category === "bash" ? [CODEWHALE_SHELL_TOOL] : pathCategoryTools(category, rule.action);
53077
+ const presentTools = new Set(group.map((candidate) => candidate.rule.tool));
53078
+ const complete = expectedTools.every((tool) => presentTools.has(tool));
53079
+ const categoryRules = permission[category] ?? {};
53080
+ if (!complete || Object.hasOwn(categoryRules, pattern)) continue;
53081
+ permission[category] = {
53082
+ ...categoryRules,
53083
+ [pattern]: rule.action
53084
+ };
53085
+ for (const candidate of group) if (expectedTools.includes(candidate.rule.tool)) claimedIndexes.add(candidate.index);
53086
+ }
53087
+ return {
53088
+ permission,
53089
+ nativeRules: rules.filter((_rule, index) => !claimedIndexes.has(index))
53090
+ };
53091
+ }
53092
+ /**
53093
+ * Codewhale permission rules in `~/.codewhale/permissions.toml`.
53094
+ *
53095
+ * The file holds a `rules` array of typed records (`tool`, optional `command`
53096
+ * prefix or exact `path`, optional `command_exact`, optional absolute
53097
+ * `workspace`, and `action`). It is the only permission-rule source Codewhale
53098
+ * reads — the project `.codewhale/config.toml` overlay cannot carry rules — so
53099
+ * the feature is global-only.
53100
+ *
53101
+ * rulesync owns the file's rules, except for workspace-scoped ones: those are
53102
+ * the grants Codewhale's approval card remembers per repository, and they are
53103
+ * kept across a regenerate. Codewhale also appends plain `exec_shell` ask rules
53104
+ * from the approval card (`S`), which cannot be told apart from rulesync's own
53105
+ * output and are replaced; run an import first to keep them.
53106
+ *
53107
+ * @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
53108
+ * @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
53109
+ */
53110
+ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
53111
+ constructor(params) {
53112
+ super({
53113
+ ...params,
53114
+ fileContent: params.fileContent ?? ""
53115
+ });
53116
+ }
53117
+ /**
53118
+ * Codewhale's approval card writes to the same file, so this feature never
53119
+ * deletes it.
53120
+ */
53121
+ isDeletable() {
53122
+ return false;
53123
+ }
53124
+ shouldSkipCreationWhenPayloadEmpty() {
53125
+ return true;
53126
+ }
53127
+ static getSettablePaths(_options) {
53128
+ return {
53129
+ relativeDirPath: CODEWHALE_DIR,
53130
+ relativeFilePath: CODEWHALE_PERMISSIONS_FILE_NAME
53131
+ };
53132
+ }
53133
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
53134
+ if (!global) throw new Error(CODEWHALE_GLOBAL_ONLY_MESSAGE);
53135
+ warnAboutRelocatedConfig(logger);
53136
+ const paths = CodewhalePermissions.getSettablePaths();
53137
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
53138
+ const fileContent = await readFileContentOrNull(filePath) ?? "";
53139
+ const { invalidCount } = partitionRuleRecords(parsePermissionsDocument({
53140
+ fileContent,
53141
+ filePath
53142
+ }));
53143
+ 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).`);
53144
+ return new CodewhalePermissions({
53145
+ outputRoot,
53146
+ relativeDirPath: paths.relativeDirPath,
53147
+ relativeFilePath: paths.relativeFilePath,
53148
+ fileContent,
53149
+ validate,
53150
+ global: true
53151
+ });
53152
+ }
53153
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
53154
+ if (!global) throw new Error(CODEWHALE_GLOBAL_ONLY_MESSAGE);
53155
+ warnAboutRelocatedConfig(logger);
53156
+ const paths = CodewhalePermissions.getSettablePaths();
53157
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
53158
+ const existing = parsePermissionsDocument({
53159
+ fileContent: await readFileContentOrNull(filePath) ?? "",
53160
+ filePath
53161
+ });
53162
+ const generated = canonicalToCodewhaleRules({
53163
+ config: rulesyncPermissions.getJson(),
53164
+ preservedRules: collectWorkspaceScopedRules({
53165
+ document: existing,
53166
+ logger
53167
+ }),
53168
+ logger
53169
+ });
53170
+ const seen = /* @__PURE__ */ new Set();
53171
+ const rules = [];
53172
+ for (const rule of generated) {
53173
+ const key = ruleKey(rule);
53174
+ if (seen.has(key)) continue;
53175
+ seen.add(key);
53176
+ rules.push(rule);
53177
+ }
53178
+ return new CodewhalePermissions({
53179
+ outputRoot,
53180
+ relativeDirPath: paths.relativeDirPath,
53181
+ relativeFilePath: paths.relativeFilePath,
53182
+ fileContent: rules.length > 0 ? smol_toml.stringify({ rules }) : "",
53183
+ validate: true,
53184
+ global: true
53185
+ });
53186
+ }
53187
+ toRulesyncPermissions() {
53188
+ const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
53189
+ fileContent: this.getFileContent(),
53190
+ filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
53191
+ })).valid.map(withExplicitAction));
53192
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
53193
+ permission,
53194
+ ...nativeRules.length > 0 && { codewhale: { rules: nativeRules } }
53195
+ }, null, 2) });
53196
+ }
53197
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
53198
+ return new CodewhalePermissions({
53199
+ outputRoot,
53200
+ relativeDirPath,
53201
+ relativeFilePath,
53202
+ fileContent: "",
53203
+ validate: false,
53204
+ global: true
53205
+ });
53206
+ }
53207
+ };
53208
+ //#endregion
52320
53209
  //#region src/features/permissions/codexcli-permissions.ts
52321
53210
  const RULESYNC_PROFILE_NAME = "rulesync";
52322
53211
  const CODEX_WORKSPACE_ROOTS_KEY = ":workspace_roots";
@@ -53192,7 +54081,7 @@ function mapBashActionToDecision(action) {
53192
54081
  //#region src/features/permissions/commandcode-permissions.ts
53193
54082
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
53194
54083
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
53195
- const CATCH_ALL_PATTERN$9 = "*";
54084
+ const CATCH_ALL_PATTERN$10 = "*";
53196
54085
  const MCP_CANONICAL_PREFIX$6 = "mcp__";
53197
54086
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
53198
54087
  const CATEGORY_TO_COMMANDCODE_TOOL = {
@@ -53247,10 +54136,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
53247
54136
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
53248
54137
  */
53249
54138
  function buildCommandcodeRule(category, pattern) {
53250
- const catchAll = pattern === CATCH_ALL_PATTERN$9 || pattern === "";
54139
+ const catchAll = pattern === CATCH_ALL_PATTERN$10 || pattern === "";
53251
54140
  if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
53252
54141
  if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
53253
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$9 : null;
54142
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$10 : null;
53254
54143
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
53255
54144
  if (tool === void 0) return null;
53256
54145
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -53286,7 +54175,7 @@ function splitCommandcodeRule(rule) {
53286
54175
  }
53287
54176
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
53288
54177
  function isCatchAllSpecifier(inner) {
53289
- return inner === "" || inner === CATCH_ALL_PATTERN$9;
54178
+ return inner === "" || inner === CATCH_ALL_PATTERN$10;
53290
54179
  }
53291
54180
  /**
53292
54181
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -53320,34 +54209,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
53320
54209
  if (server.length === 0) return null;
53321
54210
  const everyServer = {
53322
54211
  category: "mcp",
53323
- pattern: CATCH_ALL_PATTERN$9,
54212
+ pattern: CATCH_ALL_PATTERN$10,
53324
54213
  notInAllow: true
53325
54214
  };
53326
54215
  if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
53327
- if (server === CATCH_ALL_PATTERN$9) return everyServer;
53328
- if (server.includes(CATCH_ALL_PATTERN$9)) return null;
54216
+ if (server === CATCH_ALL_PATTERN$10) return everyServer;
54217
+ if (server.includes(CATCH_ALL_PATTERN$10)) return null;
53329
54218
  return {
53330
- category: rest === "" || rest === CATCH_ALL_PATTERN$9 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
53331
- pattern: CATCH_ALL_PATTERN$9,
54219
+ category: rest === "" || rest === CATCH_ALL_PATTERN$10 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
54220
+ pattern: CATCH_ALL_PATTERN$10,
53332
54221
  notInAllow: false
53333
54222
  };
53334
54223
  }
53335
54224
  const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
53336
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$9);
54225
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$10);
53337
54226
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
53338
54227
  category,
53339
54228
  pattern: inner,
53340
54229
  notInAllow: true
53341
54230
  } : null;
53342
- if (remainder === CATCH_ALL_PATTERN$9) return everyServer;
53343
- if (rest === CATCH_ALL_PATTERN$9 && !server.includes(CATCH_ALL_PATTERN$9)) return {
54231
+ if (remainder === CATCH_ALL_PATTERN$10) return everyServer;
54232
+ if (rest === CATCH_ALL_PATTERN$10 && !server.includes(CATCH_ALL_PATTERN$10)) return {
53344
54233
  category: `${MCP_CANONICAL_PREFIX$6}${server}`,
53345
- pattern: CATCH_ALL_PATTERN$9,
54234
+ pattern: CATCH_ALL_PATTERN$10,
53346
54235
  notInAllow: true
53347
54236
  };
53348
54237
  return namesTool ? {
53349
54238
  category,
53350
- pattern: CATCH_ALL_PATTERN$9,
54239
+ pattern: CATCH_ALL_PATTERN$10,
53351
54240
  notInAllow: false
53352
54241
  } : null;
53353
54242
  }
@@ -53371,14 +54260,14 @@ function friendlyToolRule({ category, inner }) {
53371
54260
  if (category === "bash") {
53372
54261
  if (isCatchAllSpecifier(inner)) return {
53373
54262
  category,
53374
- pattern: CATCH_ALL_PATTERN$9,
54263
+ pattern: CATCH_ALL_PATTERN$10,
53375
54264
  notInAllow: false
53376
54265
  };
53377
54266
  const normalized = inner.trim().replace(/\s+/g, " ");
53378
54267
  if (normalized === "" || normalized === ":*") return null;
53379
54268
  return isCatchAllSpecifier(normalized) ? {
53380
54269
  category,
53381
- pattern: CATCH_ALL_PATTERN$9,
54270
+ pattern: CATCH_ALL_PATTERN$10,
53382
54271
  notInAllow: true
53383
54272
  } : {
53384
54273
  category,
@@ -53388,7 +54277,7 @@ function friendlyToolRule({ category, inner }) {
53388
54277
  }
53389
54278
  if (isCatchAllSpecifier(inner)) return {
53390
54279
  category,
53391
- pattern: CATCH_ALL_PATTERN$9,
54280
+ pattern: CATCH_ALL_PATTERN$10,
53392
54281
  notInAllow: false
53393
54282
  };
53394
54283
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -53407,9 +54296,9 @@ function friendlyToolRule({ category, inner }) {
53407
54296
  */
53408
54297
  function parseCommandcodeRule(rule) {
53409
54298
  const { tool, inner } = splitCommandcodeRule(rule);
53410
- if (tool === CATCH_ALL_PATTERN$9) return isCatchAllSpecifier(inner) ? {
54299
+ if (tool === CATCH_ALL_PATTERN$10) return isCatchAllSpecifier(inner) ? {
53411
54300
  category: "*",
53412
- pattern: CATCH_ALL_PATTERN$9,
54301
+ pattern: CATCH_ALL_PATTERN$10,
53413
54302
  notInAllow: true
53414
54303
  } : null;
53415
54304
  const lowered = tool.toLowerCase();
@@ -53479,7 +54368,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
53479
54368
  }).filter((rule) => {
53480
54369
  const parsed = parseCommandcodeRule(rule);
53481
54370
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
53482
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$9})`);
54371
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$10})`);
53483
54372
  if (key !== "allow" && isStricterAction({
53484
54373
  action: key,
53485
54374
  existing: replacement
@@ -53536,7 +54425,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
53536
54425
  const ranked = /* @__PURE__ */ new Map();
53537
54426
  const managedCategories = /* @__PURE__ */ new Set();
53538
54427
  for (const [category, rules] of Object.entries(permission)) {
53539
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$9);
54428
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$10);
53540
54429
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
53541
54430
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
53542
54431
  for (const [pattern, action] of Object.entries(rules)) {
@@ -53807,7 +54696,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
53807
54696
  //#endregion
53808
54697
  //#region src/features/permissions/continue-permissions.ts
53809
54698
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
53810
- const CATCH_ALL_PATTERN$8 = "*";
54699
+ const CATCH_ALL_PATTERN$9 = "*";
53811
54700
  /**
53812
54701
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
53813
54702
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -53815,7 +54704,7 @@ const CATCH_ALL_PATTERN$8 = "*";
53815
54704
  * the bare entry instead of being skipped as a pattern-specific rule.
53816
54705
  */
53817
54706
  function normalizeCatchAllPattern$1(pattern) {
53818
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
54707
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$9 : pattern;
53819
54708
  }
53820
54709
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
53821
54710
  bash: "Bash",
@@ -53852,7 +54741,7 @@ function toCanonicalToolName$6(continueName) {
53852
54741
  * `Tool(pattern)`.
53853
54742
  */
53854
54743
  function buildContinuePermissionEntry(toolName, pattern) {
53855
- return pattern === CATCH_ALL_PATTERN$8 ? toolName : `${toolName}(${pattern})`;
54744
+ return pattern === CATCH_ALL_PATTERN$9 ? toolName : `${toolName}(${pattern})`;
53856
54745
  }
53857
54746
  /**
53858
54747
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -53866,7 +54755,7 @@ function parseContinuePermissionEntry(entry) {
53866
54755
  if (toolName === "") return;
53867
54756
  return {
53868
54757
  toolName,
53869
- pattern: pattern === "" ? CATCH_ALL_PATTERN$8 : pattern
54758
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$9 : pattern
53870
54759
  };
53871
54760
  }
53872
54761
  /**
@@ -54040,7 +54929,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
54040
54929
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
54041
54930
  continue;
54042
54931
  }
54043
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$8) {
54932
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$9) {
54044
54933
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
54045
54934
  continue;
54046
54935
  }
@@ -54389,14 +55278,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
54389
55278
  };
54390
55279
  //#endregion
54391
55280
  //#region src/features/permissions/crush-permissions.ts
54392
- const CATCH_ALL_PATTERN$7 = "*";
55281
+ const CATCH_ALL_PATTERN$8 = "*";
54393
55282
  /**
54394
55283
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
54395
55284
  * argument patterns at all, so it is the only pattern a rule can carry and
54396
55285
  * still be written.
54397
55286
  */
54398
55287
  function normalizeCatchAllPattern(pattern) {
54399
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$7 : pattern;
55288
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$8 : pattern;
54400
55289
  }
54401
55290
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
54402
55291
  bash: "bash",
@@ -54595,7 +55484,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
54595
55484
  for (const [rawPattern, action] of Object.entries(rules)) {
54596
55485
  if (isPrototypePollutionKey(rawPattern)) continue;
54597
55486
  const pattern = normalizeCatchAllPattern(rawPattern);
54598
- if (pattern === CATCH_ALL_PATTERN$7) {
55487
+ if (pattern === CATCH_ALL_PATTERN$8) {
54599
55488
  catchAllAction = action;
54600
55489
  continue;
54601
55490
  }
@@ -54668,11 +55557,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
54668
55557
  };
54669
55558
  for (const entry of disabled) {
54670
55559
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
54671
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$7] = "deny";
55560
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] = "deny";
54672
55561
  }
54673
55562
  for (const entry of allowed) {
54674
55563
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
54675
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$7] ??= "allow";
55564
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$8] ??= "allow";
54676
55565
  }
54677
55566
  return { permission };
54678
55567
  }
@@ -56537,7 +57426,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
56537
57426
  //#region src/features/permissions/goose-permissions.ts
56538
57427
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
56539
57428
  const GOOSE_USER_KEY = "user";
56540
- const CATCH_ALL_PATTERN$6 = "*";
57429
+ const CATCH_ALL_PATTERN$7 = "*";
56541
57430
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
56542
57431
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
56543
57432
  shell: ["bash"],
@@ -56701,7 +57590,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
56701
57590
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
56702
57591
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
56703
57592
  for (const [pattern, action] of Object.entries(rules)) {
56704
- if (pattern !== CATCH_ALL_PATTERN$6) {
57593
+ if (pattern !== CATCH_ALL_PATTERN$7) {
56705
57594
  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.`);
56706
57595
  continue;
56707
57596
  }
@@ -56732,7 +57621,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
56732
57621
  const action = GOOSE_LIST_TO_ACTION[key];
56733
57622
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
56734
57623
  permission[category] ??= {};
56735
- permission[category][CATCH_ALL_PATTERN$6] = action;
57624
+ permission[category][CATCH_ALL_PATTERN$7] = action;
56736
57625
  }
56737
57626
  }
56738
57627
  return { permission };
@@ -56743,7 +57632,7 @@ const GROKCLI_UI_KEY = "ui";
56743
57632
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
56744
57633
  const GROKCLI_PERMISSION_KEY = "permission";
56745
57634
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
56746
- const CATCH_ALL_PATTERN$5 = "*";
57635
+ const CATCH_ALL_PATTERN$6 = "*";
56747
57636
  const MCP_CANONICAL_PREFIX$3 = "mcp__";
56748
57637
  const CATEGORY_TO_GROK_TOOL = {
56749
57638
  bash: "Bash",
@@ -56789,10 +57678,10 @@ function buildGrokEntry(category, pattern) {
56789
57678
  const remainder = category.slice(5);
56790
57679
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
56791
57680
  }
56792
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$5 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
57681
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
56793
57682
  const tool = CATEGORY_TO_GROK_TOOL[category];
56794
57683
  if (tool === void 0) return null;
56795
- return pattern === CATCH_ALL_PATTERN$5 || pattern === "" ? tool : `${tool}(${pattern})`;
57684
+ return pattern === CATCH_ALL_PATTERN$6 || pattern === "" ? tool : `${tool}(${pattern})`;
56796
57685
  }
56797
57686
  /**
56798
57687
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -56813,16 +57702,16 @@ function parseGrokEntry(entry) {
56813
57702
  }
56814
57703
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
56815
57704
  category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
56816
- pattern: CATCH_ALL_PATTERN$5
57705
+ pattern: CATCH_ALL_PATTERN$6
56817
57706
  } : {
56818
57707
  category: "mcp",
56819
- pattern: CATCH_ALL_PATTERN$5
57708
+ pattern: CATCH_ALL_PATTERN$6
56820
57709
  };
56821
57710
  const category = GROK_TOOL_TO_CATEGORY[tool];
56822
57711
  if (category === void 0) return null;
56823
57712
  return {
56824
57713
  category,
56825
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$5
57714
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$6
56826
57715
  };
56827
57716
  }
56828
57717
  /**
@@ -56843,15 +57732,15 @@ function parseGrokRule(rule) {
56843
57732
  if (typeof tool !== "string") return null;
56844
57733
  if (pattern !== void 0 && typeof pattern !== "string") return null;
56845
57734
  const trimmedPattern = pattern?.trim() ?? "";
56846
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$5;
57735
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$6;
56847
57736
  const lowerTool = tool.trim().toLowerCase();
56848
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$5 ? {
57737
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$6 ? {
56849
57738
  category: "mcp",
56850
- pattern: CATCH_ALL_PATTERN$5,
57739
+ pattern: CATCH_ALL_PATTERN$6,
56851
57740
  action
56852
57741
  } : {
56853
57742
  category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
56854
- pattern: CATCH_ALL_PATTERN$5,
57743
+ pattern: CATCH_ALL_PATTERN$6,
56855
57744
  action
56856
57745
  };
56857
57746
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -56999,7 +57888,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
56999
57888
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
57000
57889
  }
57001
57890
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
57002
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$5]: legacyModeAction(parsed) } } };
57891
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$6]: legacyModeAction(parsed) } } };
57003
57892
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
57004
57893
  }
57005
57894
  validate() {
@@ -57185,12 +58074,12 @@ function reconcileNativeDenies({ permission, category, patterns }) {
57185
58074
  deleteRulesByAction(rules, "deny");
57186
58075
  for (const pattern of patterns) rules[pattern] = "deny";
57187
58076
  }
57188
- function withoutKey(record, key) {
58077
+ function withoutKey$1(record, key) {
57189
58078
  return Object.fromEntries(Object.entries(record).filter(([entryKey]) => entryKey !== key));
57190
58079
  }
57191
58080
  function buildHermesOverride(config, provenance) {
57192
58081
  const base = isRecord$1(provenance.hermes) ? { ...provenance.hermes } : {};
57193
- const approvalsOverride = withoutKey(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
58082
+ const approvalsOverride = withoutKey$1(isRecord$1(config.approvals) ? config.approvals : {}, "deny");
57194
58083
  if (Object.keys(approvalsOverride).length > 0) base.approvals = approvalsOverride;
57195
58084
  else delete base.approvals;
57196
58085
  const security = isRecord$1(config.security) ? { ...config.security } : {};
@@ -57775,6 +58664,35 @@ function narrowSandboxToProjectScope({ authored, logger }) {
57775
58664
  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.`);
57776
58665
  return emitted;
57777
58666
  }
58667
+ const KILO_MARKDOWN_SOURCE_KEY = "markdown_source";
58668
+ /**
58669
+ * Narrow an authored `markdown_source` rule to what a *project* `kilo.jsonc`
58670
+ * can mean. Kilo only grants it when the winning pattern came from the global
58671
+ * config with the action `allow` ("Project configuration cannot grant this
58672
+ * permission"). A project pattern that wins last-match-wins therefore never
58673
+ * grants: `deny` and `ask` both block (Kilo never prompts for it), so they
58674
+ * are kept — a project config may only tighten. A project `allow` would block
58675
+ * too, the opposite of what it says, so it is dropped with a warning pointing
58676
+ * at `--global`.
58677
+ * Returns `undefined` when nothing is left to write.
58678
+ * @see https://kilo.ai/docs/customize/workflows
58679
+ * @see https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/config/external-markdown.ts
58680
+ */
58681
+ function narrowMarkdownSourceToProjectScope({ authored, logger }) {
58682
+ let emitted;
58683
+ let dropped;
58684
+ if (typeof authored === "string") {
58685
+ emitted = authored === "allow" ? void 0 : authored;
58686
+ dropped = authored === "allow" ? ["*"] : [];
58687
+ } else {
58688
+ const entries = Object.entries(asKiloRecord(authored));
58689
+ const kept = entries.filter(([, action]) => action !== "allow");
58690
+ emitted = kept.length > 0 ? Object.fromEntries(kept) : void 0;
58691
+ dropped = entries.filter(([, action]) => action === "allow").map(([pattern]) => pattern);
58692
+ }
58693
+ 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.`);
58694
+ return emitted;
58695
+ }
57778
58696
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
57779
58697
  json;
57780
58698
  constructor(params) {
@@ -57861,6 +58779,13 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
57861
58779
  ...honorAllToolsOnBash(rulesyncJson.permission),
57862
58780
  ...kiloOverride?.permission
57863
58781
  };
58782
+ if (!global && Object.hasOwn(incomingPermission, KILO_MARKDOWN_SOURCE_KEY)) {
58783
+ const narrowed = narrowMarkdownSourceToProjectScope({
58784
+ authored: incomingPermission[KILO_MARKDOWN_SOURCE_KEY],
58785
+ logger
58786
+ });
58787
+ incomingPermission[KILO_MARKDOWN_SOURCE_KEY] = narrowed;
58788
+ }
57864
58789
  const droppedDenyByKey = {};
57865
58790
  for (const [key, value] of Object.entries(incomingPermission)) {
57866
58791
  const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
@@ -57876,6 +58801,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
57876
58801
  ...existingPermission,
57877
58802
  ...incomingPermission
57878
58803
  };
58804
+ if (mergedPermission[KILO_MARKDOWN_SOURCE_KEY] === void 0) delete mergedPermission[KILO_MARKDOWN_SOURCE_KEY];
57879
58805
  const nextJson = {
57880
58806
  ...parsed,
57881
58807
  permission: mergedPermission
@@ -58552,7 +59478,7 @@ function rulesFromArrays(settings, allowKey, denyKey) {
58552
59478
  //#region src/features/permissions/lettacode-permissions.ts
58553
59479
  /** Top-level key of `.letta/settings.json` that rulesync owns. */
58554
59480
  const LETTACODE_PERMISSIONS_KEY = "permissions";
58555
- const CATCH_ALL_PATTERN$4 = "*";
59481
+ const CATCH_ALL_PATTERN$5 = "*";
58556
59482
  /** Letta Code's `Bash(prefix:*)` prefix-match suffix. */
58557
59483
  const PREFIX_SUFFIX = ":*";
58558
59484
  const CATEGORY_TO_LETTACODE_TOOL = {
@@ -58645,7 +59571,7 @@ function categoryOfRule$1(rule) {
58645
59571
  * widening it would grant commands the source never allowed.
58646
59572
  */
58647
59573
  function toLettacodeBashPayload({ pattern, action, logger }) {
58648
- if (pattern === CATCH_ALL_PATTERN$4) return "";
59574
+ if (pattern === CATCH_ALL_PATTERN$5) return "";
58649
59575
  const firstStar = pattern.indexOf("*");
58650
59576
  if (firstStar === -1) return pattern;
58651
59577
  if (pattern.endsWith(PREFIX_SUFFIX) && firstStar === pattern.length - 1) return pattern;
@@ -58666,7 +59592,7 @@ function toLettacodeRule({ category, pattern, action, logger }) {
58666
59592
  pattern,
58667
59593
  action,
58668
59594
  logger
58669
- }) : pattern === CATCH_ALL_PATTERN$4 ? "" : pattern;
59595
+ }) : pattern === CATCH_ALL_PATTERN$5 ? "" : pattern;
58670
59596
  if (payload === null) return null;
58671
59597
  return payload === "" ? tool : `${tool}(${payload})`;
58672
59598
  }
@@ -58677,7 +59603,7 @@ function fromLettacodeRule(rule) {
58677
59603
  const { payload } = splitLettacodeRule(rule);
58678
59604
  if (payload === void 0) return {
58679
59605
  category,
58680
- pattern: CATCH_ALL_PATTERN$4
59606
+ pattern: CATCH_ALL_PATTERN$5
58681
59607
  };
58682
59608
  const trimmed = payload.trim();
58683
59609
  if (trimmed === "") return null;
@@ -58685,7 +59611,7 @@ function fromLettacodeRule(rule) {
58685
59611
  const prefix = trimmed.slice(0, -2).trimEnd();
58686
59612
  return {
58687
59613
  category,
58688
- pattern: prefix === "" ? CATCH_ALL_PATTERN$4 : `${prefix} *`
59614
+ pattern: prefix === "" ? CATCH_ALL_PATTERN$5 : `${prefix} *`
58689
59615
  };
58690
59616
  }
58691
59617
  return {
@@ -59141,6 +60067,377 @@ var MimocodePermissions = class extends OpencodePermissions {
59141
60067
  static layout = MIMOCODE_LAYOUT;
59142
60068
  };
59143
60069
  //#endregion
60070
+ //#region src/features/permissions/omp-permissions.ts
60071
+ const CATCH_ALL_PATTERN$4 = "*";
60072
+ const ACTION_TO_OMP_APPROVAL = {
60073
+ allow: "allow",
60074
+ ask: "prompt",
60075
+ deny: "deny"
60076
+ };
60077
+ const OMP_APPROVAL_TO_ACTION = {
60078
+ allow: "allow",
60079
+ prompt: "ask",
60080
+ deny: "deny"
60081
+ };
60082
+ const CATEGORY_TO_OMP_TOOL = {
60083
+ websearch: "web_search",
60084
+ agent: "task"
60085
+ };
60086
+ const OMP_TOOL_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_TO_OMP_TOOL).map(([category, tool]) => [tool, category]));
60087
+ const UNSUPPORTED_CATEGORIES = /* @__PURE__ */ new Set([
60088
+ "webfetch",
60089
+ "notebookedit",
60090
+ "mcp"
60091
+ ]);
60092
+ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
60093
+ "bash",
60094
+ "read",
60095
+ "edit",
60096
+ "write",
60097
+ "grep",
60098
+ "glob",
60099
+ "web_search",
60100
+ "task"
60101
+ ]);
60102
+ /**
60103
+ * Permissions adapter for oh-my-pi (`omp`).
60104
+ *
60105
+ * oh-my-pi reads approval policy from `config.yml`: `<cwd>/.omp/config.yml` in
60106
+ * project scope and `~/.omp/agent/config.yml` (default profile) globally.
60107
+ *
60108
+ * - `tools.approval.<tool>: allow | prompt | deny` is a per-tool policy honored
60109
+ * in every approval mode. A category's catch-all `*` rule maps here
60110
+ * (`ask` → `prompt`). oh-my-pi has no per-path matcher, so a non-bash category
60111
+ * with pattern-specific rules collapses to one action (deny > ask > allow, with
60112
+ * an implicit `ask` when there is no catch-all) and a warning.
60113
+ * - `bash.patterns: [{ match, approval }]` is an ordered, first-match-wins list
60114
+ * of command globs (only `*` is a wildcard). Every `bash` rule except a
60115
+ * catch-all `allow` is written there, ordered deny → prompt → allow so that
60116
+ * the first match agrees with rulesync's deny > ask > allow precedence. A
60117
+ * catch-all `allow` goes to `tools.approval.bash` instead, because by default
60118
+ * (without `bash.allowCompoundCommands`) an `allow` pattern never approves a
60119
+ * compound command line while the tool policy does — unless the category has
60120
+ * an `ask` rule, which makes `tools.approval.bash` `prompt` (see
60121
+ * `convertBashRules`) and the catch-all `allow` a trailing pattern. oh-my-pi
60122
+ * expands no `?` or `[...]`, so a deny or ask pattern widens them to `*`.
60123
+ * - The all-tools `*` category has no key of its own: its rules restrict bash
60124
+ * commands, and its deny / ask also restrict every other tool's policy (a
60125
+ * catch-all one at its own strictness, a pattern-specific one as `prompt`).
60126
+ *
60127
+ * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
60128
+ * (the keys rulesync manages) and `bash.patterns` are rewritten; sibling keys
60129
+ * such as `tools.approvalMode` and `bash.enabled` are kept, and the file is
60130
+ * never deleted.
60131
+ *
60132
+ * @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
60133
+ * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
60134
+ */
60135
+ var OmpPermissions = class OmpPermissions extends ToolPermissions {
60136
+ constructor(params) {
60137
+ super({
60138
+ ...params,
60139
+ fileContent: params.fileContent ?? ""
60140
+ });
60141
+ }
60142
+ /** `config.yml` holds unrelated user settings, so it must not be deleted. */
60143
+ isDeletable() {
60144
+ return false;
60145
+ }
60146
+ static getSettablePaths({ global = false } = {}) {
60147
+ return {
60148
+ relativeDirPath: global ? OMP_GLOBAL_DIR : OMP_DIR,
60149
+ relativeFilePath: OMP_CONFIG_FILE_NAME
60150
+ };
60151
+ }
60152
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
60153
+ const paths = OmpPermissions.getSettablePaths({ global });
60154
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
60155
+ return new OmpPermissions({
60156
+ outputRoot,
60157
+ relativeDirPath: paths.relativeDirPath,
60158
+ relativeFilePath: paths.relativeFilePath,
60159
+ fileContent,
60160
+ validate,
60161
+ global
60162
+ });
60163
+ }
60164
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
60165
+ const paths = OmpPermissions.getSettablePaths({ global });
60166
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
60167
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
60168
+ const existing = parseSharedConfig({
60169
+ format: "yaml",
60170
+ fileContent: existingContent,
60171
+ filePath
60172
+ });
60173
+ const { approval, patterns, floor } = convertRulesyncToOmp({
60174
+ config: rulesyncPermissions.getJson(),
60175
+ logger
60176
+ });
60177
+ const existingTools = isRecord$1(existing.tools) ? existing.tools : {};
60178
+ const nextApproval = {
60179
+ ...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)])) : {},
60180
+ ...approval
60181
+ };
60182
+ const toolsSiblings = withoutKey(existingTools, "approval");
60183
+ const nextTools = Object.keys(nextApproval).length > 0 ? {
60184
+ ...toolsSiblings,
60185
+ approval: nextApproval
60186
+ } : toolsSiblings;
60187
+ const bashSiblings = withoutKey(isRecord$1(existing.bash) ? existing.bash : {}, "patterns");
60188
+ const nextBash = patterns.length > 0 ? {
60189
+ ...bashSiblings,
60190
+ patterns
60191
+ } : bashSiblings;
60192
+ return new OmpPermissions({
60193
+ outputRoot,
60194
+ relativeDirPath: paths.relativeDirPath,
60195
+ relativeFilePath: paths.relativeFilePath,
60196
+ fileContent: applySharedConfigPatch({
60197
+ fileKey: sharedConfigFileKey(paths),
60198
+ feature: "permissions",
60199
+ existingContent,
60200
+ patch: {
60201
+ tools: Object.keys(nextTools).length > 0 ? nextTools : void 0,
60202
+ bash: Object.keys(nextBash).length > 0 ? nextBash : void 0
60203
+ },
60204
+ filePath,
60205
+ logger
60206
+ }),
60207
+ validate: true,
60208
+ global
60209
+ });
60210
+ }
60211
+ toRulesyncPermissions() {
60212
+ let config;
60213
+ try {
60214
+ config = parseSharedConfig({
60215
+ format: "yaml",
60216
+ fileContent: this.getFileContent()
60217
+ });
60218
+ } catch (error) {
60219
+ throw new Error(`Failed to parse oh-my-pi config in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
60220
+ }
60221
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(convertOmpToRulesync(config), null, 2) });
60222
+ }
60223
+ validate() {
60224
+ return {
60225
+ success: true,
60226
+ error: null
60227
+ };
60228
+ }
60229
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
60230
+ return new OmpPermissions({
60231
+ outputRoot,
60232
+ relativeDirPath,
60233
+ relativeFilePath,
60234
+ fileContent: "",
60235
+ validate: false,
60236
+ global
60237
+ });
60238
+ }
60239
+ };
60240
+ function withoutKey(record, key) {
60241
+ return Object.fromEntries(Object.entries(record).filter(([name]) => name !== key));
60242
+ }
60243
+ function renamed(map, name) {
60244
+ return Object.hasOwn(map, name) ? map[name] ?? name : name;
60245
+ }
60246
+ function stricter(a, b) {
60247
+ return a !== void 0 && PERMISSION_ACTION_PRIORITY[a] >= PERMISSION_ACTION_PRIORITY[b] ? a : b;
60248
+ }
60249
+ /**
60250
+ * The floor the all-tools `*` category puts under every tool's single policy:
60251
+ * a catch-all deny / ask at its own strictness, and a pattern-specific one as
60252
+ * `ask`, since some input of each tool may match it and a per-tool policy
60253
+ * cannot single that input out. `undefined` when `*` restricts nothing.
60254
+ */
60255
+ function allToolsFloor(rules) {
60256
+ let floor;
60257
+ let patternSpecific = false;
60258
+ for (const [pattern, action] of Object.entries(rules ?? {})) {
60259
+ if (action === "allow" || isPrototypePollutionKey(pattern)) continue;
60260
+ if (pattern === CATCH_ALL_PATTERN$4) floor = stricter(floor, action);
60261
+ else {
60262
+ patternSpecific = true;
60263
+ floor = stricter(floor, "ask");
60264
+ }
60265
+ }
60266
+ return {
60267
+ floor,
60268
+ patternSpecific
60269
+ };
60270
+ }
60271
+ /**
60272
+ * oh-my-pi expands only `*`, so a `?` or `[...]` in a deny / ask pattern widens
60273
+ * to `*` (a literal match would restrict almost nothing). An allow pattern is
60274
+ * left as is: matched literally it approves less, never more.
60275
+ */
60276
+ function widenRestrictionPattern(pattern) {
60277
+ return pattern.replace(/\[!?\]?[^\]]*\]|\[|\?/gu, "*").replace(/\*{2,}/gu, "*");
60278
+ }
60279
+ /**
60280
+ * A `bash` category as `bash.patterns` entries (unsorted), plus the
60281
+ * `tools.approval.bash` policy it needs.
60282
+ *
60283
+ * oh-my-pi replaces a `prompt` (or `allow`) pattern match on a critical command
60284
+ * such as `rm -rf /` with an override that carries no policy, which yolo mode
60285
+ * then resolves by `tools.approval.bash` alone. So any `ask` rule sets the tool
60286
+ * policy to `prompt`, and a catch-all `allow` then becomes a trailing
60287
+ * `match: "*"` allow pattern (patterns beat the tool policy) instead of an
60288
+ * `allow` policy that would wave such a command through.
60289
+ */
60290
+ function convertBashRules$1({ rules, logger }) {
60291
+ const patterns = [];
60292
+ const entries = Object.entries(rules).filter(([pattern]) => !isPrototypePollutionKey(pattern));
60293
+ const hasAsk = entries.some(([, action]) => action === "ask");
60294
+ let policy = hasAsk ? "ask" : void 0;
60295
+ 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.");
60296
+ for (const [pattern, action] of entries) {
60297
+ if (pattern === CATCH_ALL_PATTERN$4 && action === "allow") {
60298
+ if (hasAsk) patterns.push({
60299
+ match: CATCH_ALL_PATTERN$4,
60300
+ approval: "allow"
60301
+ });
60302
+ else policy = "allow";
60303
+ continue;
60304
+ }
60305
+ let match = pattern;
60306
+ if (/[?[]/u.test(pattern)) {
60307
+ if (action === "allow") logger?.warn(`oh-my-pi matches only "*" as a wildcard, so "?" and "[" in the bash allow pattern "${pattern}" are matched literally.`);
60308
+ else {
60309
+ match = widenRestrictionPattern(pattern);
60310
+ logger?.warn(`oh-my-pi matches only "*" as a wildcard, so the bash ${action} pattern "${pattern}" was widened to "${match}".`);
60311
+ }
60312
+ }
60313
+ patterns.push({
60314
+ match,
60315
+ approval: ACTION_TO_OMP_APPROVAL[action]
60316
+ });
60317
+ }
60318
+ return {
60319
+ patterns,
60320
+ policy
60321
+ };
60322
+ }
60323
+ /** Raise every non-bash tool policy to the all-tools floor (bash is handled by patterns). */
60324
+ function applyAllToolsFloor({ actions, allTools, logger }) {
60325
+ const { floor } = allTools;
60326
+ if (floor === void 0) return;
60327
+ 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.`);
60328
+ for (const tool of /* @__PURE__ */ new Set([...MANAGED_OMP_TOOLS, ...Object.keys(actions)])) if (tool !== "bash") actions[tool] = stricter(actions[tool], floor);
60329
+ }
60330
+ function convertRulesyncToOmp({ config, logger }) {
60331
+ const actions = {};
60332
+ const patterns = [];
60333
+ const permission = config.permission;
60334
+ const allTools = allToolsFloor(permission["*"]);
60335
+ const categories = permission["*"] === void 0 ? permission : {
60336
+ ...permission,
60337
+ bash: bashRulesHonoringAllTools(permission)
60338
+ };
60339
+ for (const [category, rules] of Object.entries(categories)) {
60340
+ if (isPrototypePollutionKey(category) || Object.keys(rules).length === 0) continue;
60341
+ if (category === "*") {
60342
+ if (Object.values(rules).includes("allow")) logger?.warn("oh-my-pi has no all-tools approval key, so the \"*\" category allow rules were skipped.");
60343
+ continue;
60344
+ }
60345
+ if (UNSUPPORTED_CATEGORIES.has(category)) {
60346
+ 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`)." : "."));
60347
+ continue;
60348
+ }
60349
+ if (category === "bash") {
60350
+ const bash = convertBashRules$1({
60351
+ rules,
60352
+ logger
60353
+ });
60354
+ patterns.push(...bash.patterns);
60355
+ if (bash.policy !== void 0) actions.bash = bash.policy;
60356
+ continue;
60357
+ }
60358
+ const action = collapseRulesToSingleAction({ rules });
60359
+ if (action === void 0) continue;
60360
+ 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).`);
60361
+ const tool = renamed(CATEGORY_TO_OMP_TOOL, category);
60362
+ 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.`);
60363
+ actions[tool] = stricter(actions[tool], action);
60364
+ }
60365
+ applyAllToolsFloor({
60366
+ actions,
60367
+ allTools,
60368
+ logger
60369
+ });
60370
+ const order = (pattern) => -PERMISSION_ACTION_PRIORITY[OMP_APPROVAL_TO_ACTION[pattern.approval]];
60371
+ patterns.sort((a, b) => order(a) - order(b));
60372
+ return {
60373
+ approval: Object.fromEntries(Object.entries(actions).map(([tool, action]) => [tool, ACTION_TO_OMP_APPROVAL[action]])),
60374
+ patterns,
60375
+ floor: allTools.floor
60376
+ };
60377
+ }
60378
+ /** An existing `tools.approval` value raised to the all-tools floor, if any. */
60379
+ function raisedApproval(value, floor) {
60380
+ if (floor === void 0) return value;
60381
+ const approval = toOmpApproval(value);
60382
+ const current = approval === void 0 ? void 0 : OMP_APPROVAL_TO_ACTION[approval];
60383
+ return ACTION_TO_OMP_APPROVAL[stricter(current, floor)];
60384
+ }
60385
+ function toOmpApproval(value) {
60386
+ if (typeof value !== "string") return;
60387
+ const normalized = value.trim().toLowerCase();
60388
+ return Object.hasOwn(OMP_APPROVAL_TO_ACTION, normalized) ? normalized : void 0;
60389
+ }
60390
+ /**
60391
+ * Fold `tools.approval.bash` (`undefined` when unset) into the imported `bash`
60392
+ * rules. A user `deny` is absolute. Otherwise a `*` pattern beats the tool
60393
+ * policy, which then reaches only compound and critical command lines — so a
60394
+ * `*` allow pattern imports as `allow` only behind an explicit `allow` policy
60395
+ * or in the shape rulesync writes (a `prompt` policy next to `ask` patterns).
60396
+ * Any other `*` allow pattern still prompts for compound lines and imports as
60397
+ * `ask`, so a regenerate cannot widen it into an `allow` policy. A `prompt`
60398
+ * policy with no `*` pattern but an `ask` pattern is what rulesync writes for
60399
+ * ask rules (see `convertBashRules`), so it adds no catch-all.
60400
+ */
60401
+ function importBashPolicy(bashRules, action) {
60402
+ const catchAll = bashRules[CATCH_ALL_PATTERN$4];
60403
+ const hasAskPattern = Object.values(bashRules).includes("ask");
60404
+ if (action === "deny") bashRules[CATCH_ALL_PATTERN$4] = "deny";
60405
+ else if (catchAll === void 0) {
60406
+ if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = action;
60407
+ } else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$4] = "ask";
60408
+ }
60409
+ /** `bash.patterns` as canonical `bash` rules, in first-match order. */
60410
+ function importBashPatterns(value) {
60411
+ const bashRules = {};
60412
+ if (!Array.isArray(value)) return bashRules;
60413
+ for (const entry of value) {
60414
+ if (!isRecord$1(entry) || typeof entry.match !== "string") continue;
60415
+ const match = entry.match.trim().replace(/\s+/gu, " ");
60416
+ const approval = toOmpApproval(entry.approval);
60417
+ if (match === "" || approval === void 0 || Object.hasOwn(bashRules, match)) continue;
60418
+ if (isPrototypePollutionKey(match)) continue;
60419
+ bashRules[match] = OMP_APPROVAL_TO_ACTION[approval];
60420
+ }
60421
+ return bashRules;
60422
+ }
60423
+ function convertOmpToRulesync(config) {
60424
+ const permission = {};
60425
+ const tools = isRecord$1(config.tools) ? config.tools : {};
60426
+ const bashRules = importBashPatterns((isRecord$1(config.bash) ? config.bash : {}).patterns);
60427
+ const bashApproval = toOmpApproval((isRecord$1(tools.approval) ? tools.approval : {}).bash);
60428
+ importBashPolicy(bashRules, bashApproval === void 0 ? void 0 : OMP_APPROVAL_TO_ACTION[bashApproval]);
60429
+ if (isRecord$1(tools.approval)) for (const [tool, value] of Object.entries(tools.approval)) {
60430
+ const approval = toOmpApproval(value);
60431
+ if (approval === void 0 || tool.trim() === "" || isPrototypePollutionKey(tool)) continue;
60432
+ const action = OMP_APPROVAL_TO_ACTION[approval];
60433
+ if (tool === "bash") continue;
60434
+ const category = renamed(OMP_TOOL_TO_CATEGORY, tool);
60435
+ permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
60436
+ }
60437
+ if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
60438
+ return { permission };
60439
+ }
60440
+ //#endregion
59144
60441
  //#region src/features/permissions/pi-permissions.ts
59145
60442
  /** The one `settings.json` key this feature owns. */
59146
60443
  const DEFAULT_TOOLS_KEY = "defaultTools";
@@ -60168,7 +61465,9 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
60168
61465
  "eager",
60169
61466
  "listDirectory",
60170
61467
  "todoWrite",
60171
- "workflowsEnabled"
61468
+ "workflowsEnabled",
61469
+ "workflowNameOnly",
61470
+ "executionSandbox"
60172
61471
  ];
60173
61472
  const QWEN_OVERRIDE_SECURITY_KEYS = [
60174
61473
  "folderTrust",
@@ -60237,6 +61536,16 @@ const QWEN_OVERRIDE_GROUPS = [{
60237
61536
  overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
60238
61537
  scopedKeys: {
60239
61538
  workflowsEnabled: { rule: "workspace-stripped" },
61539
+ executionSandbox: {
61540
+ rule: "workspace-stripped",
61541
+ grants: () => true,
61542
+ 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)."
61543
+ },
61544
+ workflowNameOnly: {
61545
+ rule: "global-machine-wide",
61546
+ 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.`,
61547
+ 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."
61548
+ },
60240
61549
  approvalMode: {
60241
61550
  rule: "global-machine-wide",
60242
61551
  projectNote: autonomyProjectNote
@@ -66055,6 +67364,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
66055
67364
  supportsImport: true
66056
67365
  }
66057
67366
  }],
67367
+ ["codewhale", {
67368
+ class: CodewhalePermissions,
67369
+ meta: {
67370
+ supportsProject: false,
67371
+ supportsGlobal: true,
67372
+ supportsImport: true
67373
+ }
67374
+ }],
66058
67375
  ["codexcli", {
66059
67376
  class: CodexcliPermissions,
66060
67377
  meta: {
@@ -66223,6 +67540,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
66223
67540
  supportsImport: true
66224
67541
  }
66225
67542
  }],
67543
+ ["omp", {
67544
+ class: OmpPermissions,
67545
+ meta: {
67546
+ supportsProject: true,
67547
+ supportsGlobal: true,
67548
+ supportsImport: true
67549
+ }
67550
+ }],
66226
67551
  ["opencode", {
66227
67552
  class: OpencodePermissions,
66228
67553
  meta: {
@@ -67102,6 +68427,10 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
67102
68427
  continue;
67103
68428
  }
67104
68429
  try {
68430
+ await assertWritablePathInsideRoot({
68431
+ rootPath: this.outputRoot,
68432
+ targetPath: root
68433
+ });
67105
68434
  await assertWritablePathInsideRoot({
67106
68435
  rootPath: this.outputRoot,
67107
68436
  targetPath: dirPath
@@ -67133,7 +68462,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
67133
68462
  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`);
67134
68463
  continue;
67135
68464
  }
67136
- if (isClaimed(filePath)) continue;
68465
+ if (await isClaimed(filePath)) continue;
67137
68466
  orphanPaths.add(filePath);
67138
68467
  }
67139
68468
  }
@@ -75689,16 +77018,30 @@ var SkillsProcessor = class extends DirFeatureProcessor {
75689
77018
  * path and vetted as writable inside this run's output root. Shared by the
75690
77019
  * two halves of the orphan sweep so both look in exactly the same places,
75691
77020
  * under exactly the same guard.
77021
+ *
77022
+ * A root that fails the guard — a symbolic link, even one that stays inside
77023
+ * the output root, such as a dotfiles checkout linked from the home
77024
+ * directory — is left out with a warning rather than failing the run. The
77025
+ * write path already wrote through such a link, and the sweep runs after
77026
+ * those writes land, so throwing here would report a failure for a run whose
77027
+ * output is in place. Sweeping through the link is not an option either: it
77028
+ * can lead to a directory rulesync does not manage, and its entries would be
77029
+ * deleted as orphans.
75692
77030
  */
75693
77031
  async loadExistingSkillsRoots(paths) {
75694
77032
  const existingRoots = [];
75695
77033
  for (const root of toolSkillSearchRoots(paths)) {
75696
77034
  const skillsDirPath = (0, node_path.join)(this.outputRoot, root);
75697
77035
  if (!await directoryExists(skillsDirPath)) continue;
75698
- await assertWritablePathInsideRoot({
75699
- rootPath: this.outputRoot,
75700
- targetPath: skillsDirPath
75701
- });
77036
+ try {
77037
+ await assertWritablePathInsideRoot({
77038
+ rootPath: this.outputRoot,
77039
+ targetPath: skillsDirPath
77040
+ });
77041
+ } catch (error) {
77042
+ this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
77043
+ continue;
77044
+ }
75702
77045
  existingRoots.push({
75703
77046
  root,
75704
77047
  skillsDirPath
@@ -76805,6 +78148,288 @@ var AugmentcodePluginSubagent = class extends AugmentcodeSubagent {
76805
78148
  }
76806
78149
  };
76807
78150
  //#endregion
78151
+ //#region src/features/subagents/roo-subagent.ts
78152
+ /**
78153
+ * Default tool groups assigned to a generated mode when the rulesync source
78154
+ * does not specify a `roo.groups` override. Mirrors Roo's built-in code mode
78155
+ * (full read/edit/command/mcp access; browser is opt-in).
78156
+ */
78157
+ const DEFAULT_GROUPS = [
78158
+ "read",
78159
+ "edit",
78160
+ "command",
78161
+ "mcp"
78162
+ ];
78163
+ /**
78164
+ * One entry of a mode's `groups` array. Either a bare toolset name
78165
+ * (`"read"`, `"edit"`, `"command"`, `"mcp"`, `"browser"`) or a tuple pairing a
78166
+ * toolset (in practice `"edit"`) with a file restriction object.
78167
+ * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
78168
+ */
78169
+ const RooModeGroupSchema = zod_mini.z.union([zod_mini.z.string(), zod_mini.z.tuple([zod_mini.z.string(), zod_mini.z.looseObject({})])]);
78170
+ /**
78171
+ * A single custom mode inside the `customModes` array of `.roomodes`.
78172
+ * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
78173
+ */
78174
+ const RooModeSchema = zod_mini.z.looseObject({
78175
+ slug: zod_mini.z.string(),
78176
+ name: zod_mini.z.string(),
78177
+ description: zod_mini.z.optional(zod_mini.z.string()),
78178
+ roleDefinition: zod_mini.z.string(),
78179
+ whenToUse: zod_mini.z.optional(zod_mini.z.string()),
78180
+ groups: zod_mini.z.optional(zod_mini.z.array(RooModeGroupSchema)),
78181
+ customInstructions: zod_mini.z.optional(zod_mini.z.string())
78182
+ });
78183
+ const RooModesFileSchema = zod_mini.z.looseObject({ customModes: zod_mini.z.array(RooModeSchema) });
78184
+ /**
78185
+ * Sanitize a string into a valid Roo mode slug (`^[a-zA-Z0-9-]+$`): lowercase,
78186
+ * non-alphanumeric runs collapsed to a single hyphen, leading/trailing hyphens
78187
+ * trimmed. Falls back to `"mode"` if nothing usable remains.
78188
+ */
78189
+ function sanitizeRooSlug(raw) {
78190
+ const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
78191
+ return slug.length > 0 ? slug : "mode";
78192
+ }
78193
+ function stringifyRooModes(modes) {
78194
+ return (0, js_yaml.dump)({ customModes: modes }, {
78195
+ lineWidth: -1,
78196
+ noRefs: true
78197
+ });
78198
+ }
78199
+ /**
78200
+ * Roo Code custom-modes adapter.
78201
+ *
78202
+ * Roo reads project-level custom modes from a single aggregated `.roomodes`
78203
+ * file at the workspace root (`docs.roocode.com` 301-redirects to the docs
78204
+ * below). Each rulesync subagent becomes one entry in the `customModes` array.
78205
+ * Because the native format aggregates N subagents into ONE file, every
78206
+ * `RooSubagent` instance carries the FULL `customModes` array and targets the
78207
+ * same `.roomodes` path; generation builds the aggregate once via
78208
+ * {@link RooSubagent.fromRulesyncSubagents}.
78209
+ *
78210
+ * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
78211
+ */
78212
+ var RooSubagent = class extends ToolSubagent {
78213
+ /** Name of the aggregated custom-modes file inside `getSettablePaths().relativeDirPath`. */
78214
+ static modesFileName = ROO_MODES_FILE_NAME;
78215
+ /** Frontmatter section of a rulesync subagent that carries the mode fields. */
78216
+ static modeSectionKey = "roo";
78217
+ /** Tool groups a generated mode gets when its section sets no `groups`. */
78218
+ static defaultGroups = DEFAULT_GROUPS;
78219
+ /** Tool target written into the frontmatter of imported subagents. */
78220
+ static importTarget = "roo";
78221
+ modes;
78222
+ constructor({ modes, ...rest }) {
78223
+ if (rest.validate !== false) {
78224
+ const result = RooModesFileSchema.safeParse({ customModes: modes });
78225
+ if (!result.success) throw new Error(`Invalid custom modes in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
78226
+ }
78227
+ super({
78228
+ ...rest,
78229
+ fileContent: stringifyRooModes(modes)
78230
+ });
78231
+ this.modes = modes;
78232
+ }
78233
+ static getSettablePaths(_options = {}) {
78234
+ return { relativeDirPath: "." };
78235
+ }
78236
+ getModes() {
78237
+ return this.modes;
78238
+ }
78239
+ getBody() {
78240
+ return this.fileContent;
78241
+ }
78242
+ /**
78243
+ * Map a single rulesync subagent to a Roo custom mode. The body becomes
78244
+ * `roleDefinition`; the optional `roo:` frontmatter section (the
78245
+ * {@link RooSubagent.modeSectionKey} section) supplies
78246
+ * `groups`, `whenToUse`, `customInstructions`, an explicit `slug`, and may
78247
+ * override `roleDefinition`. rulesync `name`/`description` are preferred,
78248
+ * while tool-specific `roo:` values take precedence where defined.
78249
+ */
78250
+ static toRooMode(rulesyncSubagent) {
78251
+ const frontmatter = rulesyncSubagent.getFrontmatter();
78252
+ const rawSection = frontmatter[this.modeSectionKey] ?? {};
78253
+ const slug = typeof rawSection.slug === "string" && rawSection.slug.length > 0 ? sanitizeRooSlug(rawSection.slug) : sanitizeRooSlug((0, node_path.basename)(rulesyncSubagent.getRelativeFilePath()).replace(/\.[^.]+$/, ""));
78254
+ const roleDefinition = typeof rawSection.roleDefinition === "string" ? rawSection.roleDefinition : rulesyncSubagent.getBody();
78255
+ const groups = Array.isArray(rawSection.groups) ? rawSection.groups : [...this.defaultGroups];
78256
+ const mode = {
78257
+ slug,
78258
+ name: frontmatter.name,
78259
+ roleDefinition,
78260
+ groups
78261
+ };
78262
+ if (frontmatter.description) mode.description = frontmatter.description;
78263
+ if (typeof rawSection.whenToUse === "string") mode.whenToUse = rawSection.whenToUse;
78264
+ if (typeof rawSection.customInstructions === "string") mode.customInstructions = rawSection.customInstructions;
78265
+ return mode;
78266
+ }
78267
+ /**
78268
+ * Aggregate every targeted rulesync subagent into a single `.roomodes` file.
78269
+ * Modes are de-duplicated by `slug` (last one wins) to keep the array unique.
78270
+ */
78271
+ static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true }) {
78272
+ const bySlug = /* @__PURE__ */ new Map();
78273
+ for (const rulesyncSubagent of rulesyncSubagents) {
78274
+ const mode = this.toRooMode(rulesyncSubagent);
78275
+ bySlug.set(mode.slug, mode);
78276
+ }
78277
+ return new this({
78278
+ outputRoot,
78279
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
78280
+ relativeFilePath: this.modesFileName,
78281
+ modes: [...bySlug.values()],
78282
+ validate
78283
+ });
78284
+ }
78285
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true }) {
78286
+ return this.fromRulesyncSubagents({
78287
+ outputRoot,
78288
+ rulesyncSubagents: [rulesyncSubagent],
78289
+ validate
78290
+ });
78291
+ }
78292
+ /**
78293
+ * Convert every custom mode in the aggregated `.roomodes` file back into an
78294
+ * individual rulesync subagent. The processor calls this in the import
78295
+ * direction (one `.roomodes` tool file fans out to N rulesync files).
78296
+ */
78297
+ toRulesyncSubagents() {
78298
+ const { modeSectionKey, importTarget } = this.constructor;
78299
+ return this.modes.map((mode) => {
78300
+ const { slug, name, description, roleDefinition, whenToUse, groups, customInstructions, ...rest } = mode;
78301
+ const modeSection = {
78302
+ ...rest,
78303
+ slug,
78304
+ ...groups ? { groups } : {},
78305
+ ...whenToUse !== void 0 ? { whenToUse } : {},
78306
+ ...customInstructions !== void 0 ? { customInstructions } : {}
78307
+ };
78308
+ return new RulesyncSubagent({
78309
+ outputRoot: ".",
78310
+ frontmatter: {
78311
+ targets: [importTarget],
78312
+ name,
78313
+ ...description !== void 0 ? { description } : {},
78314
+ [modeSectionKey]: modeSection
78315
+ },
78316
+ body: roleDefinition,
78317
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
78318
+ relativeFilePath: `${sanitizeRooSlug(slug)}.md`,
78319
+ validate: true
78320
+ });
78321
+ });
78322
+ }
78323
+ toRulesyncSubagent() {
78324
+ const first = this.toRulesyncSubagents()[0];
78325
+ if (!first) throw new Error(`No custom modes found in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} to convert.`);
78326
+ return first;
78327
+ }
78328
+ validate() {
78329
+ const result = RooModesFileSchema.safeParse({ customModes: this.modes });
78330
+ if (result.success) return {
78331
+ success: true,
78332
+ error: null
78333
+ };
78334
+ return {
78335
+ success: false,
78336
+ error: /* @__PURE__ */ new Error(`Invalid custom modes in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
78337
+ };
78338
+ }
78339
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
78340
+ return this.isTargetedByRulesyncSubagentDefault({
78341
+ rulesyncSubagent,
78342
+ toolTarget: "roo"
78343
+ });
78344
+ }
78345
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
78346
+ const paths = this.getSettablePaths();
78347
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
78348
+ const fileContent = await readFileContent(filePath);
78349
+ let parsed;
78350
+ try {
78351
+ parsed = loadYaml(fileContent);
78352
+ } catch (error) {
78353
+ throw new Error(`Failed to parse custom modes (${filePath}): ${formatError(error)}`, { cause: error });
78354
+ }
78355
+ const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
78356
+ if (!result.success) throw new Error(`Invalid custom modes in ${filePath}: ${formatError(result.error)}`);
78357
+ return new this({
78358
+ outputRoot,
78359
+ relativeDirPath: paths.relativeDirPath,
78360
+ relativeFilePath: (0, node_path.basename)(relativeFilePath),
78361
+ modes: result.data.customModes,
78362
+ validate
78363
+ });
78364
+ }
78365
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
78366
+ return new this({
78367
+ outputRoot,
78368
+ relativeDirPath,
78369
+ relativeFilePath,
78370
+ modes: [],
78371
+ validate: false
78372
+ });
78373
+ }
78374
+ };
78375
+ //#endregion
78376
+ //#region src/features/subagents/bob-subagent.ts
78377
+ /**
78378
+ * Subagent (custom-mode) generator for **IBM Bob** (Bob IDE and Bob Shell).
78379
+ *
78380
+ * Bob has no file-based subagents — its native subagents are the two fixed
78381
+ * presets `explore` and `general` — but it reads custom modes from an
78382
+ * aggregated `customModes` YAML file in Roo's format, so every targeted rulesync
78383
+ * subagent becomes one mode in `.bob/custom_modes.yaml`, exactly as the `roo`
78384
+ * target fills `.roomodes`. Only the project file is emitted: the Bob IDE and
78385
+ * Bob Shell docs name different global files.
78386
+ *
78387
+ * The mode fields ride a `bob:` frontmatter section rather than Roo's `roo:`
78388
+ * one, because Bob's tool groups are spelled differently (`execute` where Roo
78389
+ * says `command`, plus `skill`, `workflow`, `todo`, `subtask`, `subagent` and
78390
+ * `mode`), so a `roo.groups` list is not valid for Bob. A mode without
78391
+ * `bob.groups` gets `["read", "edit", "execute", "mcp"]`, Roo's default with
78392
+ * Bob IDE's spelling — the one Bob's shared approval settings use too. The Bob
78393
+ * Shell custom-modes page still lists Roo's `command` / `browser` groups, so a
78394
+ * mode meant for the Shell should spell its groups out in `bob.groups`.
78395
+ * `bob.allowedSubagents` (Bob IDE) is emitted when it is a string list, and
78396
+ * every other mode field is lifted back into `bob:` on import.
78397
+ *
78398
+ * @see https://bob.ibm.com/docs/ide/configuration/custom-modes
78399
+ * @see https://bob.ibm.com/docs/shell/configuration/custom-modes-bobshell
78400
+ * @see https://bob.ibm.com/docs/ide/features/subagents
78401
+ */
78402
+ var BobSubagent = class extends RooSubagent {
78403
+ static modesFileName = BOB_CUSTOM_MODES_FILE_NAME;
78404
+ static modeSectionKey = "bob";
78405
+ static defaultGroups = [
78406
+ "read",
78407
+ "edit",
78408
+ "execute",
78409
+ "mcp"
78410
+ ];
78411
+ static importTarget = "bob";
78412
+ static getSettablePaths(_options = {}) {
78413
+ return {
78414
+ relativeDirPath: BOB_DIR,
78415
+ relativeFilePath: BOB_CUSTOM_MODES_FILE_NAME
78416
+ };
78417
+ }
78418
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
78419
+ return this.isTargetedByRulesyncSubagentDefault({
78420
+ rulesyncSubagent,
78421
+ toolTarget: "bob"
78422
+ });
78423
+ }
78424
+ static toRooMode(rulesyncSubagent) {
78425
+ const mode = super.toRooMode(rulesyncSubagent);
78426
+ const frontmatter = rulesyncSubagent.getFrontmatter();
78427
+ const bobSection = isRecord$1(frontmatter.bob) ? frontmatter.bob : {};
78428
+ if (isStringArray$2(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
78429
+ return mode;
78430
+ }
78431
+ };
78432
+ //#endregion
76808
78433
  //#region src/features/subagents/claudecode-subagent.ts
76809
78434
  const ClaudecodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
76810
78435
  name: zod_mini.z.string(),
@@ -78552,15 +80177,14 @@ var DevinSubagent = class extends ToolSubagent {
78552
80177
  toRulesyncSubagent() {
78553
80178
  const { name, description, ...restFields } = this.frontmatter;
78554
80179
  const devinSection = { ...restFields };
78555
- const rulesyncFrontmatter = {
78556
- targets: ["*"],
78557
- name,
78558
- description,
78559
- ...Object.keys(devinSection).length > 0 && { devin: devinSection }
78560
- };
78561
80180
  return new RulesyncSubagent({
78562
- outputRoot: this.getOutputRoot(),
78563
- frontmatter: rulesyncFrontmatter,
80181
+ outputRoot: ".",
80182
+ frontmatter: {
80183
+ targets: ["*"],
80184
+ name,
80185
+ description,
80186
+ ...Object.keys(devinSection).length > 0 && { devin: devinSection }
80187
+ },
78564
80188
  body: this.body,
78565
80189
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
78566
80190
  relativeFilePath: `${this.getSubagentName()}.md`,
@@ -81329,221 +82953,6 @@ var ReasonixSubagent = class ReasonixSubagent extends ToolSubagent {
81329
82953
  }
81330
82954
  };
81331
82955
  //#endregion
81332
- //#region src/features/subagents/roo-subagent.ts
81333
- /**
81334
- * Default tool groups assigned to a generated mode when the rulesync source
81335
- * does not specify a `roo.groups` override. Mirrors Roo's built-in code mode
81336
- * (full read/edit/command/mcp access; browser is opt-in).
81337
- */
81338
- const DEFAULT_GROUPS = [
81339
- "read",
81340
- "edit",
81341
- "command",
81342
- "mcp"
81343
- ];
81344
- /**
81345
- * One entry of a mode's `groups` array. Either a bare toolset name
81346
- * (`"read"`, `"edit"`, `"command"`, `"mcp"`, `"browser"`) or a tuple pairing a
81347
- * toolset (in practice `"edit"`) with a file restriction object.
81348
- * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
81349
- */
81350
- const RooModeGroupSchema = zod_mini.z.union([zod_mini.z.string(), zod_mini.z.tuple([zod_mini.z.string(), zod_mini.z.looseObject({})])]);
81351
- /**
81352
- * A single custom mode inside the `customModes` array of `.roomodes`.
81353
- * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
81354
- */
81355
- const RooModeSchema = zod_mini.z.looseObject({
81356
- slug: zod_mini.z.string(),
81357
- name: zod_mini.z.string(),
81358
- description: zod_mini.z.optional(zod_mini.z.string()),
81359
- roleDefinition: zod_mini.z.string(),
81360
- whenToUse: zod_mini.z.optional(zod_mini.z.string()),
81361
- groups: zod_mini.z.optional(zod_mini.z.array(RooModeGroupSchema)),
81362
- customInstructions: zod_mini.z.optional(zod_mini.z.string())
81363
- });
81364
- const RooModesFileSchema = zod_mini.z.looseObject({ customModes: zod_mini.z.array(RooModeSchema) });
81365
- /**
81366
- * Sanitize a string into a valid Roo mode slug (`^[a-zA-Z0-9-]+$`): lowercase,
81367
- * non-alphanumeric runs collapsed to a single hyphen, leading/trailing hyphens
81368
- * trimmed. Falls back to `"mode"` if nothing usable remains.
81369
- */
81370
- function sanitizeRooSlug(raw) {
81371
- const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
81372
- return slug.length > 0 ? slug : "mode";
81373
- }
81374
- function stringifyRooModes(modes) {
81375
- return (0, js_yaml.dump)({ customModes: modes }, {
81376
- lineWidth: -1,
81377
- noRefs: true
81378
- });
81379
- }
81380
- /**
81381
- * Roo Code custom-modes adapter.
81382
- *
81383
- * Roo reads project-level custom modes from a single aggregated `.roomodes`
81384
- * file at the workspace root (`docs.roocode.com` 301-redirects to the docs
81385
- * below). Each rulesync subagent becomes one entry in the `customModes` array.
81386
- * Because the native format aggregates N subagents into ONE file, every
81387
- * `RooSubagent` instance carries the FULL `customModes` array and targets the
81388
- * same `.roomodes` path; generation builds the aggregate once via
81389
- * {@link RooSubagent.fromRulesyncSubagents}.
81390
- *
81391
- * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
81392
- */
81393
- var RooSubagent = class extends ToolSubagent {
81394
- modes;
81395
- constructor({ modes, ...rest }) {
81396
- if (rest.validate !== false) {
81397
- const result = RooModesFileSchema.safeParse({ customModes: modes });
81398
- if (!result.success) throw new Error(`Invalid .roomodes in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
81399
- }
81400
- super({
81401
- ...rest,
81402
- fileContent: stringifyRooModes(modes)
81403
- });
81404
- this.modes = modes;
81405
- }
81406
- static getSettablePaths(_options = {}) {
81407
- return { relativeDirPath: "." };
81408
- }
81409
- getModes() {
81410
- return this.modes;
81411
- }
81412
- getBody() {
81413
- return this.fileContent;
81414
- }
81415
- /**
81416
- * Map a single rulesync subagent to a Roo custom mode. The body becomes
81417
- * `roleDefinition`; the optional `roo:` frontmatter section supplies
81418
- * `groups`, `whenToUse`, `customInstructions`, an explicit `slug`, and may
81419
- * override `roleDefinition`. rulesync `name`/`description` are preferred,
81420
- * while tool-specific `roo:` values take precedence where defined.
81421
- */
81422
- static toRooMode(rulesyncSubagent) {
81423
- const frontmatter = rulesyncSubagent.getFrontmatter();
81424
- const rawSection = frontmatter.roo ?? {};
81425
- const slug = typeof rawSection.slug === "string" && rawSection.slug.length > 0 ? sanitizeRooSlug(rawSection.slug) : sanitizeRooSlug((0, node_path.basename)(rulesyncSubagent.getRelativeFilePath()).replace(/\.[^.]+$/, ""));
81426
- const roleDefinition = typeof rawSection.roleDefinition === "string" ? rawSection.roleDefinition : rulesyncSubagent.getBody();
81427
- const groups = Array.isArray(rawSection.groups) ? rawSection.groups : [...DEFAULT_GROUPS];
81428
- const mode = {
81429
- slug,
81430
- name: frontmatter.name,
81431
- roleDefinition,
81432
- groups
81433
- };
81434
- if (frontmatter.description) mode.description = frontmatter.description;
81435
- if (typeof rawSection.whenToUse === "string") mode.whenToUse = rawSection.whenToUse;
81436
- if (typeof rawSection.customInstructions === "string") mode.customInstructions = rawSection.customInstructions;
81437
- return mode;
81438
- }
81439
- /**
81440
- * Aggregate every targeted rulesync subagent into a single `.roomodes` file.
81441
- * Modes are de-duplicated by `slug` (last one wins) to keep the array unique.
81442
- */
81443
- static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true }) {
81444
- const bySlug = /* @__PURE__ */ new Map();
81445
- for (const rulesyncSubagent of rulesyncSubagents) {
81446
- const mode = this.toRooMode(rulesyncSubagent);
81447
- bySlug.set(mode.slug, mode);
81448
- }
81449
- return new this({
81450
- outputRoot,
81451
- relativeDirPath: this.getSettablePaths().relativeDirPath,
81452
- relativeFilePath: ROO_MODES_FILE_NAME,
81453
- modes: [...bySlug.values()],
81454
- validate
81455
- });
81456
- }
81457
- static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true }) {
81458
- return this.fromRulesyncSubagents({
81459
- outputRoot,
81460
- rulesyncSubagents: [rulesyncSubagent],
81461
- validate
81462
- });
81463
- }
81464
- /**
81465
- * Convert every custom mode in the aggregated `.roomodes` file back into an
81466
- * individual rulesync subagent. The processor calls this in the import
81467
- * direction (one `.roomodes` tool file fans out to N rulesync files).
81468
- */
81469
- toRulesyncSubagents() {
81470
- return this.modes.map((mode) => {
81471
- const { slug, name, description, roleDefinition, whenToUse, groups, customInstructions, ...rest } = mode;
81472
- const rooSection = {
81473
- ...rest,
81474
- slug,
81475
- ...groups ? { groups } : {},
81476
- ...whenToUse !== void 0 ? { whenToUse } : {},
81477
- ...customInstructions !== void 0 ? { customInstructions } : {}
81478
- };
81479
- return new RulesyncSubagent({
81480
- outputRoot: ".",
81481
- frontmatter: {
81482
- targets: ["roo"],
81483
- name,
81484
- ...description !== void 0 ? { description } : {},
81485
- roo: rooSection
81486
- },
81487
- body: roleDefinition,
81488
- relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
81489
- relativeFilePath: `${sanitizeRooSlug(slug)}.md`,
81490
- validate: true
81491
- });
81492
- });
81493
- }
81494
- toRulesyncSubagent() {
81495
- const first = this.toRulesyncSubagents()[0];
81496
- if (!first) throw new Error("No custom modes found in .roomodes to convert.");
81497
- return first;
81498
- }
81499
- validate() {
81500
- const result = RooModesFileSchema.safeParse({ customModes: this.modes });
81501
- if (result.success) return {
81502
- success: true,
81503
- error: null
81504
- };
81505
- return {
81506
- success: false,
81507
- error: /* @__PURE__ */ new Error(`Invalid .roomodes in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
81508
- };
81509
- }
81510
- static isTargetedByRulesyncSubagent(rulesyncSubagent) {
81511
- return this.isTargetedByRulesyncSubagentDefault({
81512
- rulesyncSubagent,
81513
- toolTarget: "roo"
81514
- });
81515
- }
81516
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
81517
- const paths = this.getSettablePaths();
81518
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
81519
- const fileContent = await readFileContent(filePath);
81520
- let parsed;
81521
- try {
81522
- parsed = loadYaml(fileContent);
81523
- } catch (error) {
81524
- throw new Error(`Failed to parse .roomodes (${filePath}): ${formatError(error)}`, { cause: error });
81525
- }
81526
- const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
81527
- if (!result.success) throw new Error(`Invalid .roomodes in ${filePath}: ${formatError(result.error)}`);
81528
- return new this({
81529
- outputRoot,
81530
- relativeDirPath: paths.relativeDirPath,
81531
- relativeFilePath: (0, node_path.basename)(relativeFilePath),
81532
- modes: result.data.customModes,
81533
- validate
81534
- });
81535
- }
81536
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
81537
- return new this({
81538
- outputRoot,
81539
- relativeDirPath,
81540
- relativeFilePath,
81541
- modes: [],
81542
- validate: false
81543
- });
81544
- }
81545
- };
81546
- //#endregion
81547
82956
  //#region src/features/subagents/tabnine-subagent.ts
81548
82957
  const TabnineSubagentFrontmatterSchema = zod_mini.z.looseObject({
81549
82958
  name: zod_mini.z.string(),
@@ -82041,20 +83450,19 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
82041
83450
  const resolvedBody = this.resolvedSystemPrompt ?? system_prompt;
82042
83451
  const unresolvedPromptId = this.resolvedSystemPrompt === void 0 ? system_prompt_id : void 0;
82043
83452
  const fileStem = (0, node_path.basename)(this.getRelativeFilePath(), ".toml");
82044
- const rulesyncFrontmatter = {
82045
- targets: ["vibe"],
82046
- name: display_name ?? fileStem,
82047
- ...description !== void 0 && { description },
82048
- vibe: {
82049
- ...display_name !== void 0 && { display_name },
82050
- ...description !== void 0 && { description },
82051
- ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
82052
- ...vibeSection
82053
- }
82054
- };
82055
83453
  return new RulesyncSubagent({
82056
- outputRoot: this.outputRoot,
82057
- frontmatter: rulesyncFrontmatter,
83454
+ outputRoot: ".",
83455
+ frontmatter: {
83456
+ targets: ["vibe"],
83457
+ name: display_name ?? fileStem,
83458
+ ...description !== void 0 && { description },
83459
+ vibe: {
83460
+ ...display_name !== void 0 && { display_name },
83461
+ ...description !== void 0 && { description },
83462
+ ...unresolvedPromptId !== void 0 && { system_prompt_id: unresolvedPromptId },
83463
+ ...vibeSection
83464
+ }
83465
+ },
82058
83466
  body: resolvedBody ?? "",
82059
83467
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
82060
83468
  relativeFilePath: this.getRelativeFilePath().replace(/\.toml$/, ".md"),
@@ -82487,6 +83895,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
82487
83895
  filePattern: "*.md"
82488
83896
  }
82489
83897
  }],
83898
+ ["bob", {
83899
+ class: BobSubagent,
83900
+ meta: {
83901
+ supportsProject: true,
83902
+ supportsSimulated: false,
83903
+ supportsGlobal: false,
83904
+ filePattern: "custom_modes.yaml"
83905
+ }
83906
+ }],
82490
83907
  ["claudecode", {
82491
83908
  class: ClaudecodeSubagent,
82492
83909
  meta: {
@@ -83109,10 +84526,15 @@ var SubagentsProcessor = class extends FeatureProcessor {
83109
84526
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
83110
84527
  const dirPath = typeof root === "string" ? root : root.relativeDirPath;
83111
84528
  const baseDir = (0, node_path.join)(rootOutputRoot, dirPath);
83112
- if (forDeletion && await directoryExists(baseDir)) await assertWritablePathInsideRoot({
83113
- rootPath: rootOutputRoot,
83114
- targetPath: baseDir
83115
- });
84529
+ if (forDeletion && await directoryExists(baseDir)) try {
84530
+ await assertWritablePathInsideRoot({
84531
+ rootPath: rootOutputRoot,
84532
+ targetPath: baseDir
84533
+ });
84534
+ } catch (error) {
84535
+ this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(baseDir)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
84536
+ continue;
84537
+ }
83116
84538
  const subagentFilePaths = (await findFilesByGlobs(factory.meta.filePattern, {
83117
84539
  cwd: baseDir,
83118
84540
  followSymbolicLinks: !forDeletion
@@ -84074,6 +85496,16 @@ const AntigravityRuleFrontmatterSchema = zod_mini.z.looseObject({
84074
85496
  description: zod_mini.z.optional(zod_mini.z.string())
84075
85497
  });
84076
85498
  /**
85499
+ * File name of a non-root rule in the shared global `~/.gemini/config/rules/`
85500
+ * directory. Antigravity reads only its top level, so a nested rulesync rule
85501
+ * (`frontend/style.md`) is flattened into a single file name
85502
+ * (`frontend-style.md`). The IDE and the CLI both use it so they write the
85503
+ * same file for the same rule.
85504
+ */
85505
+ function toGlobalRuleFileName(relativeFilePath) {
85506
+ return relativeFilePath.split(/[\\/]/).join("-");
85507
+ }
85508
+ /**
84077
85509
  * Converts a comma-separated globs string or array to an array of globs.
84078
85510
  * @param globs - Comma-separated globs string (e.g., "*.ts,*.js") or array of globs
84079
85511
  * @returns Array of glob patterns
@@ -84304,7 +85736,7 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
84304
85736
  });
84305
85737
  return new AntigravityCliRule({
84306
85738
  ...params,
84307
- relativeFilePath: params.relativeFilePath.split(/[\\/]/).join("-"),
85739
+ relativeFilePath: toGlobalRuleFileName(params.relativeFilePath),
84308
85740
  fileContent,
84309
85741
  global
84310
85742
  });
@@ -84402,10 +85834,14 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
84402
85834
  *
84403
85835
  * - Project scope: every rule is placed as a non-root file in
84404
85836
  * `.agents/rules/` with Antigravity trigger frontmatter.
84405
- * - Global scope: a single plain `~/.gemini/GEMINI.md` root file (shared with
84406
- * the Antigravity CLI), without frontmatter.
85837
+ * - Global scope: a plain `~/.gemini/GEMINI.md` root file without
85838
+ * frontmatter, and non-root rules with trigger frontmatter in
85839
+ * `~/.gemini/config/rules/`. Both are shared with the Antigravity CLI, which
85840
+ * writes the same files for the same rules.
85841
+ *
85842
+ * @see https://antigravity.google/docs/rules
84407
85843
  */
84408
- var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
85844
+ var AntigravityIdeRule = class extends ToolRule {
84409
85845
  frontmatter;
84410
85846
  body;
84411
85847
  constructor({ frontmatter, body, ...rest }) {
@@ -84433,28 +85869,18 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84433
85869
  };
84434
85870
  }
84435
85871
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
84436
- if (global) return { root: this.getGlobalRootPath(excludeToolDir) };
85872
+ if (global) return {
85873
+ root: this.getGlobalRootPath(excludeToolDir),
85874
+ nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_GEMINI_DIR, ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR, excludeToolDir) }
85875
+ };
84437
85876
  return {
84438
85877
  root: this.getProjectRootPath(),
84439
85878
  nonRoot: { relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_AGENTS_DIR, "rules", excludeToolDir) }
84440
85879
  };
84441
85880
  }
84442
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
85881
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
84443
85882
  const paths = this.getSettablePaths({ global });
84444
- if (global) {
84445
- const rootPath = paths.root;
84446
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
84447
- return new this({
84448
- outputRoot,
84449
- relativeDirPath: rootPath.relativeDirPath,
84450
- relativeFilePath: rootPath.relativeFilePath,
84451
- frontmatter: {},
84452
- body: fileContent,
84453
- validate,
84454
- root: true
84455
- });
84456
- }
84457
- if (relativeFilePath === paths.root.relativeFilePath) {
85883
+ if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
84458
85884
  const rootPath = paths.root;
84459
85885
  const rootContent = await readFileContent((0, node_path.join)(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath));
84460
85886
  return new this({
@@ -84464,10 +85890,10 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84464
85890
  frontmatter: {},
84465
85891
  body: rootContent,
84466
85892
  validate,
84467
- root: true
85893
+ root: true,
85894
+ global
84468
85895
  });
84469
85896
  }
84470
- if (!("nonRoot" in paths) || !paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
84471
85897
  const nonRootDirPath = paths.nonRoot.relativeDirPath;
84472
85898
  const filePath = (0, node_path.join)(outputRoot, nonRootDirPath, relativeFilePath);
84473
85899
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
@@ -84484,23 +85910,12 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84484
85910
  body,
84485
85911
  frontmatter: parsedFrontmatter,
84486
85912
  validate,
84487
- root: false
85913
+ root: false,
85914
+ global
84488
85915
  });
84489
85916
  }
84490
85917
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
84491
85918
  const paths = this.getSettablePaths({ global });
84492
- if (global) {
84493
- const rootPath = paths.root;
84494
- return new this({
84495
- outputRoot,
84496
- relativeDirPath: rootPath.relativeDirPath,
84497
- relativeFilePath: rootPath.relativeFilePath,
84498
- frontmatter: {},
84499
- body: rulesyncRule.getBody(),
84500
- validate,
84501
- root: true
84502
- });
84503
- }
84504
85919
  if (rulesyncRule.getFrontmatter().root) {
84505
85920
  const rootPath = paths.root;
84506
85921
  return new this({
@@ -84510,7 +85925,8 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84510
85925
  frontmatter: {},
84511
85926
  body: rulesyncRule.getBody(),
84512
85927
  validate,
84513
- root: true
85928
+ root: true,
85929
+ global
84514
85930
  });
84515
85931
  }
84516
85932
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
@@ -84520,16 +85936,16 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84520
85936
  const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
84521
85937
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
84522
85938
  const frontmatter = strategy.generateFrontmatter(normalized, rulesyncFrontmatter);
84523
- if (!("nonRoot" in paths) || !paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
84524
- const kebabCaseFilename = toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
85939
+ const relativeFilePath = global ? toGlobalRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
84525
85940
  return new this({
84526
85941
  outputRoot,
84527
85942
  relativeDirPath: paths.nonRoot.relativeDirPath,
84528
- relativeFilePath: kebabCaseFilename,
85943
+ relativeFilePath,
84529
85944
  frontmatter,
84530
85945
  body: rulesyncRule.getBody(),
84531
85946
  validate,
84532
- root: false
85947
+ root: false,
85948
+ global
84533
85949
  });
84534
85950
  }
84535
85951
  toRulesyncRule() {
@@ -84574,15 +85990,26 @@ var AntigravityIdeRule = class AntigravityIdeRule extends ToolRule {
84574
85990
  error: null
84575
85991
  };
84576
85992
  }
85993
+ /**
85994
+ * `~/.gemini/config/rules/` is shared with the Antigravity CLI and holds
85995
+ * global rules the user created outside rulesync, which cannot be told apart
85996
+ * from generated ones, so the orphan sweep never removes files there.
85997
+ */
85998
+ isDeletable() {
85999
+ return !this.global || this.root;
86000
+ }
84577
86001
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
84578
- return new AntigravityIdeRule({
86002
+ const rootPath = this.getSettablePaths({ global }).root;
86003
+ const isRoot = relativeFilePath === rootPath.relativeFilePath && relativeDirPath === rootPath.relativeDirPath;
86004
+ return new this({
84579
86005
  outputRoot,
84580
86006
  relativeDirPath,
84581
86007
  relativeFilePath,
84582
86008
  frontmatter: {},
84583
86009
  body: "",
84584
86010
  validate: false,
84585
- root: global || relativeFilePath === "AGENTS.md" && relativeDirPath === "."
86011
+ root: isRoot,
86012
+ global
84586
86013
  });
84587
86014
  }
84588
86015
  static isTargetedByRulesyncRule(rulesyncRule) {
@@ -91263,7 +92690,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
91263
92690
  meta: {
91264
92691
  extension: "md",
91265
92692
  supportsGlobal: true,
91266
- ruleDiscoveryMode: "auto"
92693
+ ruleDiscoveryMode: "auto",
92694
+ sharedGlobalNonRootDir: true
91267
92695
  }
91268
92696
  }],
91269
92697
  ["antigravity-plugin", {
@@ -91900,6 +93328,7 @@ var RulesProcessor = class extends FeatureProcessor {
91900
93328
  toolRules,
91901
93329
  factory
91902
93330
  });
93331
+ await this.warnForWarpLegacyRootFile(toolRules);
91903
93332
  this.warnForInstructionBudget({
91904
93333
  toolRules,
91905
93334
  meta
@@ -92313,6 +93742,24 @@ var RulesProcessor = class extends FeatureProcessor {
92313
93742
  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.`);
92314
93743
  }
92315
93744
  /**
93745
+ * Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
93746
+ * `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so the
93747
+ * `AGENTS.md` this run writes would never be read. Rulesync neither writes
93748
+ * nor deletes `WARP.md` (it is hand-authored), so the only thing it can do is
93749
+ * say so. Project scope only: the documented precedence is about project
93750
+ * rules, and Warp's global rule source is `~/.agents/AGENTS.md`.
93751
+ * @see https://docs.warp.dev/agents/capabilities/rules/
93752
+ */
93753
+ async warnForWarpLegacyRootFile(toolRules) {
93754
+ if (this.toolTarget !== "warp" || this.global) return;
93755
+ if (toolRules.length === 0) return;
93756
+ const { root } = WarpRule.getSettablePaths();
93757
+ const legacyRelativePath = (0, node_path.join)(root.relativeDirPath, WARP_LEGACY_RULE_FILE_NAME);
93758
+ if (!await fileExists((0, node_path.join)(this.outputRoot, legacyRelativePath))) return;
93759
+ const rootRelativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
93760
+ 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}.`);
93761
+ }
93762
+ /**
92316
93763
  * Handle localRoot rule generation based on tool target.
92317
93764
  * - `separate-local-file`: writes a dedicated `*.local.md` root file
92318
93765
  * (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
@@ -93694,26 +95141,64 @@ function createFoldRootOverwriteWatch({ logger }) {
93694
95141
  }
93695
95142
  //#endregion
93696
95143
  //#region src/lib/orphan-sweep.ts
93697
- function createOrphanSweepPlan() {
95144
+ function createOrphanSweepPlan({ logger } = {}) {
93698
95145
  const generatedPaths = /* @__PURE__ */ new Set();
93699
95146
  const generatedTrees = /* @__PURE__ */ new Set();
93700
95147
  const deferredSweeps = [];
93701
95148
  const deletedPathsByFeature = /* @__PURE__ */ new Map();
93702
- const isInsideGeneratedTree = (absolutePath) => {
93703
- let current = absolutePath;
95149
+ const warnedUnresolvable = /* @__PURE__ */ new Set();
95150
+ let landingClaims;
95151
+ const isInsideTree = ({ path, trees }) => {
95152
+ let current = path;
93704
95153
  let parent = (0, node_path.dirname)(current);
93705
95154
  while (parent !== current) {
93706
- if (generatedTrees.has(parent)) return true;
95155
+ if (trees.has(parent)) return true;
93707
95156
  current = parent;
93708
95157
  parent = (0, node_path.dirname)(current);
93709
95158
  }
93710
95159
  return false;
93711
95160
  };
95161
+ const followLinks = async (path) => {
95162
+ try {
95163
+ return await writeLandingPath(path);
95164
+ } catch {
95165
+ return null;
95166
+ }
95167
+ };
95168
+ const landingsOf = async (paths) => {
95169
+ const landings = await Promise.all([...paths].map((path) => followLinks(path)));
95170
+ return new Set(landings.filter((landing) => landing !== null));
95171
+ };
95172
+ const getLandingClaims = () => {
95173
+ landingClaims ??= Promise.all([landingsOf(generatedPaths), landingsOf(generatedTrees)]).then(([paths, trees]) => ({
95174
+ paths,
95175
+ trees: new Set([...trees].filter((tree) => (0, node_path.dirname)(tree) !== tree))
95176
+ }));
95177
+ return landingClaims;
95178
+ };
95179
+ const landsOnClaim = async ({ path, exactly }) => {
95180
+ const resolved = (0, node_path.resolve)(path);
95181
+ const landing = await followLinks(resolved);
95182
+ if (landing === null) {
95183
+ if (!warnedUnresolvable.has(resolved)) {
95184
+ warnedUnresolvable.add(resolved);
95185
+ logger?.warn(`Refusing to sweep ${quoteForLog(resolved)}: its symbolic links cannot be followed (a link cycle or an unreadable directory)`);
95186
+ }
95187
+ return true;
95188
+ }
95189
+ const claims = await getLandingClaims();
95190
+ return claims.paths.has(landing) || !exactly && isInsideTree({
95191
+ path: landing,
95192
+ trees: claims.trees
95193
+ });
95194
+ };
93712
95195
  const plan = {
93713
95196
  registerGenerated({ paths }) {
95197
+ landingClaims = void 0;
93714
95198
  for (const path of paths) generatedPaths.add((0, node_path.resolve)(path));
93715
95199
  },
93716
95200
  registerGeneratedTree({ paths }) {
95201
+ landingClaims = void 0;
93717
95202
  for (const path of paths) {
93718
95203
  const resolved = (0, node_path.resolve)(path);
93719
95204
  generatedPaths.add(resolved);
@@ -93722,13 +95207,26 @@ function createOrphanSweepPlan() {
93722
95207
  },
93723
95208
  isGenerated({ path }) {
93724
95209
  const resolved = (0, node_path.resolve)(path);
93725
- return generatedPaths.has(resolved) || isInsideGeneratedTree(resolved);
95210
+ return generatedPaths.has(resolved) || isInsideTree({
95211
+ path: resolved,
95212
+ trees: generatedTrees
95213
+ });
93726
95214
  },
93727
- isGeneratedExactly({ path }) {
93728
- return generatedPaths.has((0, node_path.resolve)(path));
95215
+ async isGeneratedExactly({ path }) {
95216
+ return generatedPaths.has((0, node_path.resolve)(path)) || await landsOnClaim({
95217
+ path,
95218
+ exactly: true
95219
+ });
93729
95220
  },
93730
- rejectClaimed({ items, getPath }) {
93731
- return items.filter((item) => !plan.isGenerated({ path: getPath(item) }));
95221
+ async rejectClaimed({ items, getPath }) {
95222
+ const claimed = await Promise.all(items.map(async (item) => {
95223
+ const path = getPath(item);
95224
+ return plan.isGenerated({ path }) || await landsOnClaim({
95225
+ path,
95226
+ exactly: false
95227
+ });
95228
+ }));
95229
+ return items.filter((_, index) => !claimed[index]);
93732
95230
  },
93733
95231
  defer({ sweep, reportDeleted }) {
93734
95232
  deferredSweeps.push({
@@ -93922,19 +95420,19 @@ function scheduleRetiredTargetSweeps({ config, logger, sweepPlan, sourceLoadFail
93922
95420
  }
93923
95421
  }
93924
95422
  async function sweepFiles({ processor, sweepPlan, unownedPaths }) {
93925
- const candidates = sweepPlan.rejectClaimed({
95423
+ const candidates = (await sweepPlan.rejectClaimed({
93926
95424
  items: await processor.loadToolFiles({ forDeletion: true }),
93927
95425
  getPath: (f) => f.getFilePath()
93928
- }).filter((f) => !unownedPaths.has((0, node_path.resolve)(f.getFilePath())));
95426
+ })).filter((f) => !unownedPaths.has((0, node_path.resolve)(f.getFilePath())));
93929
95427
  const present = await Promise.all(candidates.map((f) => fileExists(f.getFilePath())));
93930
95428
  return await processor.removeOrphanAiFiles(candidates.filter((_, index) => present[index]), []) > 0;
93931
95429
  }
93932
95430
  async function sweepDirs({ processor, sweepPlan }) {
93933
- return await processor.removeOrphanAiDirs(sweepPlan.rejectClaimed({
95431
+ return await processor.removeOrphanAiDirs(await sweepPlan.rejectClaimed({
93934
95432
  items: await processor.loadToolDirsToDelete(),
93935
95433
  getPath: (d) => d.getDirPath()
93936
95434
  }), []) + await processor.removeOrphanFlatFiles({
93937
- existingFlatFiles: sweepPlan.rejectClaimed({
95435
+ existingFlatFiles: await sweepPlan.rejectClaimed({
93938
95436
  items: await processor.loadToolFlatFilesToDelete(),
93939
95437
  getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
93940
95438
  }),
@@ -94121,7 +95619,7 @@ async function processFeatureGeneration(params) {
94121
95619
  if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
94122
95620
  sweep: async () => {
94123
95621
  const existingToolFiles = await processor.loadToolFiles({ forDeletion: true });
94124
- return await processor.removeOrphanAiFiles(sweepPlan.rejectClaimed({
95622
+ return await processor.removeOrphanAiFiles(await sweepPlan.rejectClaimed({
94125
95623
  items: existingToolFiles,
94126
95624
  getPath: (f) => f.getFilePath()
94127
95625
  }), toolFiles) > 0;
@@ -94135,6 +95633,16 @@ async function processFeatureGeneration(params) {
94135
95633
  sourceLoadFailed: processor.hasRulesyncSourceLoadFailure()
94136
95634
  };
94137
95635
  }
95636
+ /**
95637
+ * Whether a write to `targetPath` lands strictly below `rootPath` once every
95638
+ * link on the way to either is followed. A cycle on either side answers false.
95639
+ */
95640
+ async function landsStrictlyBelowRoot({ rootPath, targetPath }) {
95641
+ const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
95642
+ if (rootLanding === null || targetLanding === null) return false;
95643
+ const relativePath = (0, node_path.relative)(rootLanding, targetLanding);
95644
+ return relativePath !== "" && !pathEscapesRoot(relativePath);
95645
+ }
94138
95646
  async function processDirFeatureGeneration(params) {
94139
95647
  const { config, processor, toolDirs, sweepPlan } = params;
94140
95648
  let totalCount = 0;
@@ -94145,7 +95653,11 @@ async function processDirFeatureGeneration(params) {
94145
95653
  allPaths.push(...writeResult.paths);
94146
95654
  if (writeResult.count > 0) hasDiff = true;
94147
95655
  const ownedTrees = toolDirs.filter((d) => d.ownsDirTree());
94148
- sweepPlan.registerGeneratedTree({ paths: ownedTrees.map((d) => d.getDirPath()) });
95656
+ const treesBelowRoot = await Promise.all(ownedTrees.map((d) => landsStrictlyBelowRoot({
95657
+ rootPath: d.getOutputRoot(),
95658
+ targetPath: d.getDirPath()
95659
+ })));
95660
+ sweepPlan.registerGeneratedTree({ paths: ownedTrees.filter((_, index) => treesBelowRoot[index]).map((d) => d.getDirPath()) });
94149
95661
  sweepPlan.registerGenerated({ paths: toolDirs.flatMap((d) => {
94150
95662
  const dirPath = d.getDirPath();
94151
95663
  const mainFile = d.getMainFile();
@@ -94158,13 +95670,13 @@ async function processDirFeatureGeneration(params) {
94158
95670
  if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
94159
95671
  sweep: async () => {
94160
95672
  const existingToolDirs = await processor.loadToolDirsToDelete();
94161
- const orphanDirCount = await processor.removeOrphanAiDirs(sweepPlan.rejectClaimed({
95673
+ const orphanDirCount = await processor.removeOrphanAiDirs(await sweepPlan.rejectClaimed({
94162
95674
  items: existingToolDirs,
94163
95675
  getPath: (d) => d.getDirPath()
94164
95676
  }), toolDirs);
94165
95677
  const existingFlatFiles = await processor.loadToolFlatFilesToDelete();
94166
95678
  const orphanFileCount = await processor.removeOrphanFlatFiles({
94167
- existingFlatFiles: sweepPlan.rejectClaimed({
95679
+ existingFlatFiles: await sweepPlan.rejectClaimed({
94168
95680
  items: existingFlatFiles,
94169
95681
  getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
94170
95682
  }),
@@ -94191,10 +95703,10 @@ async function processEmptyFeatureGeneration(params) {
94191
95703
  if (config.getDelete() && !processor.hasRulesyncSourceLoadFailure()) sweepPlan.defer({
94192
95704
  sweep: async () => {
94193
95705
  const existingToolFiles = await processor.loadToolFiles({ forDeletion: true });
94194
- const filesToDelete = sweepPlan.rejectClaimed({
95706
+ const filesToDelete = (await sweepPlan.rejectClaimed({
94195
95707
  items: existingToolFiles,
94196
95708
  getPath: (f) => f.getFilePath()
94197
- }).filter((f) => !skipFilePaths?.has(f.getRelativePathFromCwd()));
95709
+ })).filter((f) => !skipFilePaths?.has(f.getRelativePathFromCwd()));
94198
95710
  return await processor.removeOrphanAiFiles(filesToDelete, []) > 0;
94199
95711
  },
94200
95712
  reportDeleted: () => processor.getRemovedPaths()
@@ -94520,7 +96032,7 @@ async function generate(params) {
94520
96032
  logger
94521
96033
  });
94522
96034
  let skillsResult;
94523
- const sweepPlan = createOrphanSweepPlan();
96035
+ const sweepPlan = createOrphanSweepPlan({ logger });
94524
96036
  const runners = {
94525
96037
  ignore: () => generateIgnoreCore({
94526
96038
  config,
@@ -95695,6 +97207,12 @@ Object.defineProperty(exports, "AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME", {
95695
97207
  return AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME;
95696
97208
  }
95697
97209
  });
97210
+ Object.defineProperty(exports, "CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME", {
97211
+ enumerable: true,
97212
+ get: function() {
97213
+ return CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME;
97214
+ }
97215
+ });
95698
97216
  Object.defineProperty(exports, "CLAUDECODE_DIR", {
95699
97217
  enumerable: true,
95700
97218
  get: function() {