rulesync 16.25.0 → 16.26.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.
@@ -4730,9 +4730,14 @@ const KIMI_CODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CAN
4730
4730
  * ZCode's configuration-file hooks expose exactly seven PascalCase events, all
4731
4731
  * of which have a clean canonical equivalent: `SessionStart`, `PreToolUse`,
4732
4732
  * `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `Stop`, and
4733
- * `UserPromptSubmit` ← `beforeSubmitPrompt`. An optional matcher (a
4734
- * case-sensitive regular expression) is honored on all of them except
4735
- * `UserPromptSubmit` and `Stop`, which expose no value to match against.
4733
+ * `UserPromptSubmit` ← `beforeSubmitPrompt`. An optional matcher is honored on
4734
+ * all of them except `UserPromptSubmit` and `Stop`, which expose no value to
4735
+ * match against. ZCode reads the matcher in one of two ways: a value made up
4736
+ * solely of letters, digits, underscores and `|` is an exact name list
4737
+ * (`Write|Edit` matches those two tool names, not the regex alternation), and
4738
+ * any other character makes the whole value a case-sensitive JavaScript regex.
4739
+ * Rulesync passes matchers through verbatim, so an authored value keeps
4740
+ * whichever reading ZCode gives it — worth knowing before escaping one.
4736
4741
  * Configuration hooks are read only from the user config
4737
4742
  * `~/.zcode/cli/config.json` (workspace config hooks are never executed) and
4738
4743
  * additionally require `hooks.enabled: true` to run.
@@ -6904,12 +6909,37 @@ const DeepagentsStartupOverrideSchema = zod_mini.z.looseObject({
6904
6909
  read_project_dotenv: zod_mini.z.optional(zod_mini.z.boolean())
6905
6910
  });
6906
6911
  /**
6912
+ * deepagents-cli's Python extension gate under `[extensions]` in
6913
+ * `~/.deepagents/config.toml`. `discover_extensions` auto-loads `*.py` from the
6914
+ * user's `~/.deepagents/extensions/` and from the checked-out project's
6915
+ * `<root>/.deepagents/extensions/`, so these two keys decide whether
6916
+ * repository-authored Python is imported into the agent process at all —
6917
+ * the same class of knob as `[startup].mode`, and one that ships defaulting to
6918
+ * a prompt a user can accept. Loose so a key added upstream passes through
6919
+ * verbatim.
6920
+ *
6921
+ * `extensions.extra_paths` is deliberately absent: it names machine-local files
6922
+ * and directories, which do not belong in a committed
6923
+ * `.rulesync/permissions.jsonc`, and it only ever widens what loads.
6924
+ *
6925
+ * @see https://docs.langchain.com/oss/deepagents/code/configuration
6926
+ */
6927
+ const DeepagentsExtensionsOverrideSchema = zod_mini.z.looseObject({
6928
+ enabled: zod_mini.z.optional(zod_mini.z.boolean()),
6929
+ trust: zod_mini.z.optional(zod_mini.z.enum([
6930
+ "ask",
6931
+ "always",
6932
+ "never"
6933
+ ]))
6934
+ });
6935
+ /**
6907
6936
  * The `[shell].allow_list` array itself is rulesync-owned, driven by the
6908
6937
  * shared `permission.bash` block, so it has no key here.
6909
6938
  */
6910
6939
  const DeepagentsPermissionsOverrideSchema = zod_mini.z.looseObject({
6911
6940
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
6912
- startup: zod_mini.z.optional(DeepagentsStartupOverrideSchema)
6941
+ startup: zod_mini.z.optional(DeepagentsStartupOverrideSchema),
6942
+ extensions: zod_mini.z.optional(DeepagentsExtensionsOverrideSchema)
6913
6943
  });
6914
6944
  /**
6915
6945
  * The actions Junie's allowlist accepts. Verified against the shipped Junie
@@ -7920,8 +7950,9 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
7920
7950
  agentsmd: zod_mini.z.optional(zod_mini.z.looseObject({ subprojectPath: zod_mini.z.optional(zod_mini.z.string()) })),
7921
7951
  claudecode: zod_mini.z.optional(zod_mini.z.looseObject({ paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
7922
7952
  codebuddy: zod_mini.z.optional(zod_mini.z.looseObject({
7923
- paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
7953
+ paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
7924
7954
  alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
7955
+ enabled: zod_mini.z.optional(zod_mini.z.boolean()),
7925
7956
  description: zod_mini.z.optional(zod_mini.z.string())
7926
7957
  })),
7927
7958
  cursor: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -9214,6 +9245,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
9214
9245
  license: zod_mini.z.optional(zod_mini.z.string()),
9215
9246
  compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
9216
9247
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
9248
+ })),
9249
+ zcode: zod_mini.z.optional(zod_mini.z.looseObject({
9250
+ when_to_use: zod_mini.z.optional(zod_mini.z.string()),
9251
+ license: zod_mini.z.optional(zod_mini.z.string()),
9252
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
9217
9253
  }))
9218
9254
  });
9219
9255
  /**
@@ -17796,6 +17832,20 @@ const ROO_MCP_FILE_NAME = "mcp.json";
17796
17832
  */
17797
17833
  const ROO_MODE_SLUG_PATTERN = /^[a-zA-Z0-9-]+$/;
17798
17834
  /**
17835
+ * `~/.roo/rules/AGENTS.md` — where the root rule goes in global scope.
17836
+ *
17837
+ * Roo/Zoo Code discover the agent-rules files (`AGENTS.md`, `AGENT.md`,
17838
+ * `AGENTS.local.md`) in the **workspace only**: `loadAllAgentRulesFiles` reads
17839
+ * `cwd`, plus subdirectories holding a `.roo` folder when `enableSubfolderRules`
17840
+ * is on. There is no home-directory branch, so a `~/AGENTS.md` is never read.
17841
+ * The global rules directory `~/.roo/rules/` *is* loaded, so the root rule is
17842
+ * written there alongside the non-root files rather than to an unread path.
17843
+ * The basename is kept so the file still reads as the root overview.
17844
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/prompts/sections/custom-instructions.ts
17845
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/services/roo-config/index.ts
17846
+ */
17847
+ const ROO_GLOBAL_ROOT_RULE_FILE_NAME = "AGENTS.md";
17848
+ /**
17799
17849
  * `.roo/rules-{mode}/` — the mode-specific rule directory Roo/Zoo Code load
17800
17850
  * INSTEAD of `.roo/rules/` while that mode is active. The relative path is the
17801
17851
  * same in global scope, where it resolves under `~/.roo/`.
@@ -22855,7 +22905,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
22855
22905
  * adapter so the settings reader in `src/utils/` can name the same list without
22856
22906
  * importing a feature.
22857
22907
  *
22858
- * @see https://docs.factory.ai/cli/configuration/settings
22908
+ * @see https://docs.factory.ai/droid-cli/settings
22859
22909
  */
22860
22910
  const FACTORYDROID_OVERRIDE_KEYS = [
22861
22911
  "commandBlocklist",
@@ -22863,6 +22913,8 @@ const FACTORYDROID_OVERRIDE_KEYS = [
22863
22913
  "sandbox",
22864
22914
  "mcpPolicy",
22865
22915
  "mcpAutonomyOverrides",
22916
+ "mcpAutonomyUrlOverrides",
22917
+ "builtInToolAutonomyOverrides",
22866
22918
  "enableDroidShield",
22867
22919
  "sessionDefaultSettings",
22868
22920
  "maxAutonomyLevel",
@@ -22870,7 +22922,10 @@ const FACTORYDROID_OVERRIDE_KEYS = [
22870
22922
  "interactionMode",
22871
22923
  "extraKnownMarketplaces",
22872
22924
  "enabledPlugins",
22925
+ "strictEnabledPlugins",
22926
+ "strictKnownMarketplaces",
22873
22927
  "hooksDisabled",
22928
+ "allowManagedHooksOnly",
22874
22929
  "disabledSkills",
22875
22930
  "modelPolicy",
22876
22931
  "missionPolicy"
@@ -26911,6 +26966,7 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
26911
26966
  //#region src/constants/crush-paths.ts
26912
26967
  const CRUSH_RULE_FILE_NAME = "CRUSH.md";
26913
26968
  const CRUSH_GLOBAL_DIR = (0, node_path.join)(".config", "crush");
26969
+ const CRUSH_LOCAL_RULE_FILE_NAME = "CRUSH.local.md";
26914
26970
  const CRUSH_IGNORE_FILE_NAME = ".crushignore";
26915
26971
  const CRUSH_SKILLS_PROJECT_DIR = (0, node_path.join)(".crush", "skills");
26916
26972
  const CRUSH_SKILLS_GLOBAL_DIR = (0, node_path.join)(CRUSH_GLOBAL_DIR, "skills");
@@ -33131,9 +33187,16 @@ function lookupTransport(map, key) {
33131
33187
  }
33132
33188
  /**
33133
33189
  * Read the "put this server's own instructions into the agent's prompt" flag
33134
- * off an unfiltered canonical entry, under either spelling. Anything other
33135
- * than a literal `true` reads as not enabled, matching Rovo Dev, where the key
33136
- * is absent by default.
33190
+ * off an unfiltered canonical entry, under either spelling. `undefined` means
33191
+ * the entry says nothing, which is Rovo Dev's own default.
33192
+ *
33193
+ * The flag is a **tri-state**, not a switch that only exists when on. Atlassian
33194
+ * inverted the default on 2026-09-02: a server's instructions are now surfaced
33195
+ * when the key is absent or `true`, and `false` is what suppresses them — the
33196
+ * opposite of the opt-in wording this adapter was first written against, where
33197
+ * absent and `false` meant the same thing. So `false` has to travel end to end;
33198
+ * collapsing it away is what makes a suppression impossible to author, and
33199
+ * erases one on import.
33137
33200
  *
33138
33201
  * The canonical key decides whenever it is present, the way codex resolves the
33139
33202
  * same two-spelling conflict for `experimental_environment`. OR-ing the two
@@ -33141,28 +33204,25 @@ function lookupTransport(map, key) {
33141
33204
  * `enable_instructions: true` copied out of Atlassian's docs — fail-open, on
33142
33205
  * the one key whose whole purpose is a trust decision.
33143
33206
  *
33207
+ * A non-boolean under either spelling reads as `undefined` rather than as a
33208
+ * suppression: Rovo Dev surfaces the instructions for anything that is not
33209
+ * `false`, so writing `false` there would invent a restriction the tool is not
33210
+ * applying. (The canonical key is a strict boolean in the schema, so only the
33211
+ * raw spelling can carry one.)
33212
+ *
33144
33213
  * `isPlainObject` rather than `isRecord`: this walks a user-supplied key set,
33145
33214
  * so a `constructor` entry must not resolve up the prototype chain.
33146
33215
  */
33147
33216
  function readEnableInstructions(rawServer) {
33148
- if (!isPlainObject$1(rawServer)) return false;
33149
- if (rawServer.rovodevEnableInstructions !== void 0) return rawServer.rovodevEnableInstructions === true;
33150
- return rawServer.enable_instructions === true;
33151
- }
33152
- /**
33153
- * Names that were emitted with `enable_instructions: true` during one
33154
- * `fromRulesyncMcp` call, so the run can say so once rather than per server.
33155
- * Atlassian gates this key on trust, and it is the only thing generate writes
33156
- * that widens what steers the model — the quietest possible write is the wrong
33157
- * one for it.
33158
- */
33159
- function warnEnabledInstructions(names, logger) {
33160
- if (names.length === 0) return;
33161
- logger?.warn(`Rovo Dev MCP: writing enable_instructions: true for ${names.join(", ")}. Rovo Dev pastes ${names.length === 1 ? "that server's" : "those servers'"} own instructions into the agent's system prompt, so enable it only for servers you trust.`);
33217
+ if (!isPlainObject$1(rawServer)) return;
33218
+ const canonical = rawServer.rovodevEnableInstructions;
33219
+ if (canonical !== void 0) return typeof canonical === "boolean" ? canonical : void 0;
33220
+ const raw = rawServer.enable_instructions;
33221
+ return typeof raw === "boolean" ? raw : void 0;
33162
33222
  }
33163
33223
  function toRovodevServer(name, server, logger) {
33164
33224
  const { type, transport, disabled: _disabled, rovodevEnableInstructions, ...rest } = server;
33165
- if (rovodevEnableInstructions === true) rest.enable_instructions = true;
33225
+ if (typeof rovodevEnableInstructions === "boolean") rest.enable_instructions = rovodevEnableInstructions;
33166
33226
  const declared = typeof transport === "string" ? transport : typeof type === "string" ? type : void 0;
33167
33227
  if (declared === void 0) return rest;
33168
33228
  const mapped = lookupTransport(CANONICAL_TO_ROVODEV_TRANSPORT, declared);
@@ -33177,7 +33237,7 @@ function toRovodevServer(name, server, logger) {
33177
33237
  }
33178
33238
  function fromRovodevServer(server) {
33179
33239
  const { transport, enable_instructions: enableInstructions, ...rest } = server;
33180
- if (enableInstructions === true) rest.rovodevEnableInstructions = true;
33240
+ if (typeof enableInstructions === "boolean") rest.rovodevEnableInstructions = enableInstructions;
33181
33241
  if (typeof transport !== "string") return rest;
33182
33242
  const mapped = lookupTransport(ROVODEV_TO_CANONICAL_TRANSPORT, transport);
33183
33243
  return mapped === void 0 ? rest : {
@@ -33618,10 +33678,10 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
33618
33678
  canWriteDisableToggle = false;
33619
33679
  }
33620
33680
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
33621
- const rawServer = rulesyncMcp.getRawMcpServer(name);
33681
+ const enableInstructions = readEnableInstructions(rulesyncMcp.getRawMcpServer(name));
33622
33682
  const record = {
33623
33683
  ...server,
33624
- ...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
33684
+ ...enableInstructions !== void 0 && { rovodevEnableInstructions: enableInstructions }
33625
33685
  };
33626
33686
  if (record.disabled === true && !canWriteDisableToggle) {
33627
33687
  logger?.warn(`Rovo Dev MCP: skipping disabled server "${name}" because config.yml cannot be parsed, so mcp.disabledMcpServers cannot be written to switch it off.`);
@@ -33630,7 +33690,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
33630
33690
  const converted = toRovodevServer(name, record, logger);
33631
33691
  return converted === null ? null : [name, converted];
33632
33692
  }).filter((entry) => entry !== null));
33633
- warnEnabledInstructions(Object.entries(mcpServers).filter(([, server]) => server.enable_instructions === true).map(([name]) => name), logger);
33634
33693
  const rovodevConfig = {
33635
33694
  ...json,
33636
33695
  mcpServers
@@ -39774,6 +39833,34 @@ const DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS = {
39774
39833
  yolo_switcher: true,
39775
39834
  read_project_dotenv: true
39776
39835
  };
39836
+ /**
39837
+ * `[extensions]` gates dcode's Python extension system. `discover_extensions`
39838
+ * auto-loads `*.py` from the user's `~/.deepagents/extensions/` and from the
39839
+ * checked-out project's `<root>/.deepagents/extensions/`, so this table decides
39840
+ * whether a cloned repository's Python is imported into the agent process.
39841
+ */
39842
+ const EXTENSIONS_TABLE_KEY = "extensions";
39843
+ /**
39844
+ * Keys lifted back into the override on import. `extensions.extra_paths` is
39845
+ * deliberately absent: it names machine-local files and directories, which do
39846
+ * not belong in a committed `.rulesync/permissions.jsonc`, and it only ever
39847
+ * widens what loads.
39848
+ */
39849
+ const DEEPAGENTS_EXTENSIONS_KEYS = ["enabled", "trust"];
39850
+ /** `TrustPolicy` in `extensions/settings.py`; dcode reads nothing else. */
39851
+ const DEEPAGENTS_EXTENSION_TRUST_POLICIES = [
39852
+ "ask",
39853
+ "always",
39854
+ "never"
39855
+ ];
39856
+ /**
39857
+ * `[extensions].enabled` defaults to `true` upstream, so writing `true` on a
39858
+ * machine that has not set it changes nothing — the same reasoning as
39859
+ * `DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS`.
39860
+ *
39861
+ * @see https://github.com/langchain-ai/deepagents `config_manifest.py`
39862
+ */
39863
+ const DEEPAGENTS_EXTENSIONS_ENABLED_DEFAULT = true;
39777
39864
  const ALLOW_ALL_SENTINEL = "all";
39778
39865
  const RECOMMENDED_SENTINEL = "recommended";
39779
39866
  const GLOB_CHARACTERS_PATTERN = /[*?[\]]/;
@@ -39843,7 +39930,10 @@ const TRAILING_ARGUMENT_WILDCARD_PATTERN = /:\*$/;
39843
39930
  * `DeepagentsPermissionsOverrideSchema`): `[startup].mode`
39844
39931
  * (`manual` / `auto` / `yolo`), `[startup].yolo_switcher` and
39845
39932
  * `[startup].read_project_dotenv` are merged into the file on generate and
39846
- * lifted back into the override on import.
39933
+ * lifted back into the override on import. The same override carries
39934
+ * `[extensions].enabled` and `[extensions].trust` (`ask` / `always` / `never`),
39935
+ * which decide whether the Python in a checked-out project's
39936
+ * `.deepagents/extensions/` is imported into the agent process.
39847
39937
  *
39848
39938
  * On **import** the allowlist comes back as `bash` `allow` rules named by the
39849
39939
  * executable — skipping any entry dcode could not match in the first place, so
@@ -39945,6 +40035,13 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
39945
40035
  filePath,
39946
40036
  logger
39947
40037
  });
40038
+ const extensionsOverride = config.deepagents?.extensions;
40039
+ if (isPlainObject$1(extensionsOverride) && Object.keys(extensionsOverride).length > 0) mergeExtensionsOverride({
40040
+ settings,
40041
+ extensionsOverride,
40042
+ filePath,
40043
+ logger
40044
+ });
39948
40045
  return new DeepagentsPermissions({
39949
40046
  outputRoot,
39950
40047
  relativeDirPath: paths.relativeDirPath,
@@ -39969,8 +40066,15 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
39969
40066
  startup: isPlainObject$1(settings[STARTUP_TABLE_KEY]) ? settings[STARTUP_TABLE_KEY] : {},
39970
40067
  selfPath
39971
40068
  });
40069
+ const extensionsOverride = liftExtensionsOverride({
40070
+ extensions: isPlainObject$1(settings[EXTENSIONS_TABLE_KEY]) ? settings[EXTENSIONS_TABLE_KEY] : {},
40071
+ selfPath
40072
+ });
39972
40073
  const result = { ...config };
39973
- if (Object.keys(startupOverride).length > 0) result.deepagents = { startup: startupOverride };
40074
+ const deepagents = {};
40075
+ if (Object.keys(startupOverride).length > 0) deepagents.startup = startupOverride;
40076
+ if (Object.keys(extensionsOverride).length > 0) deepagents.extensions = extensionsOverride;
40077
+ if (Object.keys(deepagents).length > 0) result.deepagents = deepagents;
39974
40078
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
39975
40079
  }
39976
40080
  validate() {
@@ -40057,57 +40161,192 @@ function warnAboutUnwrittenBashRules({ allowAll, requestedAllowAll, askPatterns,
40057
40161
  if (willWrite && allowAll) warnWithFallback(logger, "The bash '*' allow rule became allow_list = [\"all\"] for deepagents-cli, which auto-approves every command and skips its dangerous-pattern check (command substitution, redirects, process substitution). List the executables you want instead if that is more than you meant.");
40058
40162
  }
40059
40163
  /**
40060
- * Lift the `[startup]` keys rulesync models back into the `deepagents`
40061
- * override, keeping only values dcode itself accepts.
40164
+ * Lift the keys rulesync models from one `config.toml` table back into the
40165
+ * `deepagents` override, keeping only values dcode itself accepts.
40166
+ *
40167
+ * A value outside what the option's own parser reads is `Invalid` upstream —
40168
+ * dcode ignores it and falls back to its default. Importing it anyway would
40169
+ * record a setting the tool is not applying and, because the canonical schema
40170
+ * is stricter than TOML, would write a `.rulesync/permissions.jsonc` the next
40171
+ * `rulesync generate` cannot even read.
40062
40172
  *
40063
- * A `mode` outside the three approval modes, or a non-boolean where a switch
40064
- * belongs, is `Invalid` upstream dcode ignores it and falls back to its
40065
- * default. Importing it anyway would record a setting the tool is not applying
40066
- * and, because the canonical schema is stricter than TOML, would write a
40067
- * `.rulesync/permissions.jsonc` the next `rulesync generate` cannot even read.
40173
+ * `normalize` returns the value to keep, or `null` for one dcode would not
40174
+ * read a wrapper rather than the bare value, so a legitimately falsy setting
40175
+ * is not mistaken for a rejection.
40068
40176
  */
40069
- function liftStartupOverride({ startup, selfPath }) {
40070
- const startupOverride = {};
40177
+ function liftOverrideTable({ table, tableKey, keys, normalize, selfPath }) {
40178
+ const override = {};
40071
40179
  const rejected = [];
40072
- for (const key of DEEPAGENTS_STARTUP_KEYS) {
40073
- const value = startup[key];
40180
+ for (const key of keys) {
40181
+ const value = table[key];
40074
40182
  if (value === void 0) continue;
40075
- if (key === "mode" ? DEEPAGENTS_STARTUP_MODES.includes(value) : typeof value === "boolean") startupOverride[key] = value;
40183
+ const normalized = normalize({
40184
+ key,
40185
+ value
40186
+ });
40187
+ if (normalized) override[key] = normalized.value;
40076
40188
  else rejected.push(`${key} = ${JSON.stringify(value)}`);
40077
40189
  }
40078
- if (rejected.length > 0) warnWithFallback(void 0, `deepagents-cli falls back to its own default for a '[${STARTUP_TABLE_KEY}]' value it cannot read, so ${rejected.join(", ")} in ${selfPath} ${rejected.length === 1 ? "was" : "were"} not imported.`);
40079
- return startupOverride;
40190
+ if (rejected.length > 0) warnWithFallback(void 0, `deepagents-cli falls back to its own default for a '[${tableKey}]' value it cannot read, so ${rejected.join(", ")} in ${selfPath} ${rejected.length === 1 ? "was" : "were"} not imported.`);
40191
+ return override;
40080
40192
  }
40081
40193
  /**
40082
- * Merge the `deepagents.startup` override into `[startup]`, preserving every
40083
- * other key of that table. A `startup` that is not a table at all is something
40084
- * rulesync did not write and cannot merge into, so it is left exactly as the
40085
- * user has it rather than replaced.
40194
+ * Lift the `[startup]` keys rulesync models back into the `deepagents`
40195
+ * override. A `mode` outside the three approval modes, or a non-boolean where
40196
+ * a switch belongs, is one dcode ignores.
40086
40197
  */
40087
- function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
40088
- const existingStartup = settings[STARTUP_TABLE_KEY];
40089
- if (existingStartup !== void 0 && !isPlainObject$1(existingStartup)) {
40090
- warnWithFallback(logger, `deepagents-cli: '${STARTUP_TABLE_KEY}' in ${filePath} is not a table, so the deepagents startup override was skipped rather than overwriting it.`);
40198
+ function liftStartupOverride({ startup, selfPath }) {
40199
+ return liftOverrideTable({
40200
+ table: startup,
40201
+ tableKey: STARTUP_TABLE_KEY,
40202
+ keys: DEEPAGENTS_STARTUP_KEYS,
40203
+ normalize: ({ key, value }) => {
40204
+ if (key === "mode") return DEEPAGENTS_STARTUP_MODES.includes(value) ? { value } : null;
40205
+ return typeof value === "boolean" ? { value } : null;
40206
+ },
40207
+ selfPath
40208
+ });
40209
+ }
40210
+ /**
40211
+ * Lift the `[extensions]` keys rulesync models back into the `deepagents`
40212
+ * override.
40213
+ *
40214
+ * `trust` is read the way `parse_trust_policy` reads it — trimmed and
40215
+ * lowercased — so `"Always"` comes back as the policy dcode is actually
40216
+ * applying rather than being dropped, the same courtesy the allowlist
40217
+ * sentinels already get. The canonical enum holds only the lowercase
40218
+ * spellings, so keeping the value verbatim would write a permissions file the
40219
+ * next generate could not parse.
40220
+ */
40221
+ function liftExtensionsOverride({ extensions, selfPath }) {
40222
+ return liftOverrideTable({
40223
+ table: extensions,
40224
+ tableKey: EXTENSIONS_TABLE_KEY,
40225
+ keys: DEEPAGENTS_EXTENSIONS_KEYS,
40226
+ normalize: ({ key, value }) => {
40227
+ if (key === "trust") {
40228
+ if (typeof value !== "string") return null;
40229
+ const policy = value.trim().toLowerCase();
40230
+ return DEEPAGENTS_EXTENSION_TRUST_POLICIES.includes(policy) ? { value: policy } : null;
40231
+ }
40232
+ return typeof value === "boolean" ? { value } : null;
40233
+ },
40234
+ selfPath
40235
+ });
40236
+ }
40237
+ /**
40238
+ * Merge one block of the `deepagents` override into its `config.toml` table,
40239
+ * preserving every other key of that table. A table that is not a table at all
40240
+ * is something rulesync did not write and cannot merge into, so it is left
40241
+ * exactly as the user has it rather than replaced.
40242
+ */
40243
+ function mergeOverrideTable({ settings, tableKey, override, knownKeys, isDroppedKey, warnAboutRelaxations, filePath, logger }) {
40244
+ const existing = settings[tableKey];
40245
+ if (existing !== void 0 && !isPlainObject$1(existing)) {
40246
+ warnWithFallback(logger, `deepagents-cli: '${tableKey}' in ${filePath} is not a table, so the deepagents ${tableKey} override was skipped rather than overwriting it.`);
40091
40247
  return;
40092
40248
  }
40093
- const startup = isPlainObject$1(existingStartup) ? { ...existingStartup } : {};
40094
- const previousStartup = { ...startup };
40249
+ const table = isPlainObject$1(existing) ? { ...existing } : {};
40250
+ const previous = { ...table };
40095
40251
  const writtenKeys = [];
40096
- for (const [key, value] of Object.entries(startupOverride)) {
40252
+ for (const [key, value] of Object.entries(override)) {
40097
40253
  if (isPrototypePollutionKey(key)) continue;
40098
- if (key === STARTUP_RECENT_KEY) continue;
40254
+ if (isDroppedKey?.(key)) continue;
40099
40255
  if (value === null || value === void 0) continue;
40100
- startup[key] = value;
40256
+ table[key] = value;
40101
40257
  writtenKeys.push(key);
40102
40258
  }
40103
- warnAboutStartupRelaxations({
40104
- startupOverride,
40105
- previousStartup,
40259
+ warnAboutRelaxations({
40260
+ override,
40261
+ previous,
40262
+ filePath,
40263
+ logger
40264
+ });
40265
+ warnAboutUncheckedKeys({
40266
+ tableKey,
40267
+ knownKeys,
40106
40268
  writtenKeys,
40107
40269
  filePath,
40108
40270
  logger
40109
40271
  });
40110
- if (Object.keys(startup).length > 0) settings[STARTUP_TABLE_KEY] = startup;
40272
+ if (Object.keys(table).length > 0) settings[tableKey] = table;
40273
+ }
40274
+ /**
40275
+ * Name the keys the merge wrote that rulesync does not model. The override is a
40276
+ * loose object so a key added upstream still reaches the config, but this one
40277
+ * writes into the machine's global file: what rulesync cannot judge, it at
40278
+ * least names.
40279
+ */
40280
+ function warnAboutUncheckedKeys({ tableKey, knownKeys, writtenKeys, filePath, logger }) {
40281
+ const known = new Set(knownKeys);
40282
+ const unknownKeys = writtenKeys.filter((key) => !known.has(key));
40283
+ if (unknownKeys.length === 0) return;
40284
+ warnWithFallback(logger, `The deepagents ${tableKey} override wrote ${unknownKeys.join(", ")} into ${filePath} unchecked — rulesync does not know what those keys grant, and that file is your global deepagents-cli config.`);
40285
+ }
40286
+ /**
40287
+ * Merge the `deepagents.startup` override into `[startup]`.
40288
+ */
40289
+ function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
40290
+ mergeOverrideTable({
40291
+ settings,
40292
+ tableKey: STARTUP_TABLE_KEY,
40293
+ override: startupOverride,
40294
+ knownKeys: DEEPAGENTS_STARTUP_KEYS,
40295
+ isDroppedKey: (key) => key === STARTUP_RECENT_KEY,
40296
+ warnAboutRelaxations: warnAboutStartupRelaxations,
40297
+ filePath,
40298
+ logger
40299
+ });
40300
+ }
40301
+ /**
40302
+ * Merge the `deepagents.extensions` override into `[extensions]`. Nothing is
40303
+ * dropped here — `extra_paths` is not modeled, but it is a key the user may
40304
+ * legitimately want carried, so it passes through as any other unknown key
40305
+ * does, named by the unchecked-key warning.
40306
+ */
40307
+ function mergeExtensionsOverride({ settings, extensionsOverride, filePath, logger }) {
40308
+ mergeOverrideTable({
40309
+ settings,
40310
+ tableKey: EXTENSIONS_TABLE_KEY,
40311
+ override: extensionsOverride,
40312
+ knownKeys: DEEPAGENTS_EXTENSIONS_KEYS,
40313
+ warnAboutRelaxations: warnAboutExtensionsRelaxations,
40314
+ filePath,
40315
+ logger
40316
+ });
40317
+ }
40318
+ /**
40319
+ * Name the value an override key replaces, so a setting the user had turned
40320
+ * down is visible as such rather than reported as a bare new value.
40321
+ */
40322
+ function describeOverriddenValue({ key, value, previous }) {
40323
+ const previousValue = previous[key];
40324
+ return previousValue === void 0 || previousValue === value ? `${key} = ${JSON.stringify(value)}` : `${key} = ${JSON.stringify(value)} (was ${JSON.stringify(previousValue)})`;
40325
+ }
40326
+ /**
40327
+ * Warn when the `deepagents` extensions override widens what dcode loads on its
40328
+ * own, for the same reason the startup one does: this block is written into the
40329
+ * user's **global** config from a `.rulesync/permissions.jsonc` a repository
40330
+ * can carry.
40331
+ *
40332
+ * `trust` decides what happens to the Python in a checked-out project's
40333
+ * `.deepagents/extensions/` — `always` imports it into the agent process with
40334
+ * no prompt at all — and `enabled` is the switch above it. `never` and `false`
40335
+ * restrict, so neither is reported.
40336
+ */
40337
+ function warnAboutExtensionsRelaxations({ override, previous, filePath, logger }) {
40338
+ const relaxations = [];
40339
+ if (override.trust === "always") relaxations.push(describeOverriddenValue({
40340
+ key: "trust",
40341
+ value: "always",
40342
+ previous
40343
+ }));
40344
+ if (override.enabled === true && (previous.enabled ?? DEEPAGENTS_EXTENSIONS_ENABLED_DEFAULT) !== true) relaxations.push(describeOverriddenValue({
40345
+ key: "enabled",
40346
+ value: true,
40347
+ previous
40348
+ }));
40349
+ if (relaxations.length > 0) warnWithFallback(logger, `The deepagents extensions override wrote ${relaxations.join(", ")} into ${filePath}, which is your global deepagents-cli config: it decides whether the Python in a checked-out project's .deepagents/extensions/ is imported into dcode, for every project on this machine, not just this one.`);
40111
40350
  }
40112
40351
  /**
40113
40352
  * Warn when the `deepagents` startup override relaxes what dcode may do on its
@@ -40122,12 +40361,13 @@ function mergeStartupOverride({ settings, startupOverride, filePath, logger }) {
40122
40361
  * process environment. The value being replaced is named alongside, so a
40123
40362
  * setting the user had turned down is visible as such.
40124
40363
  */
40125
- function warnAboutStartupRelaxations({ startupOverride, previousStartup, writtenKeys, filePath, logger }) {
40364
+ function warnAboutStartupRelaxations({ override: startupOverride, previous: previousStartup, filePath, logger }) {
40126
40365
  const relaxations = [];
40127
- const describe = (key, value) => {
40128
- const previous = previousStartup[key];
40129
- return previous === void 0 || previous === value ? `${key} = ${JSON.stringify(value)}` : `${key} = ${JSON.stringify(value)} (was ${JSON.stringify(previous)})`;
40130
- };
40366
+ const describe = (key, value) => describeOverriddenValue({
40367
+ key,
40368
+ value,
40369
+ previous: previousStartup
40370
+ });
40131
40371
  const mode = startupOverride.mode;
40132
40372
  if (mode === "auto" || mode === "yolo") relaxations.push(describe("mode", mode));
40133
40373
  for (const [key, upstreamDefault] of Object.entries(DEEPAGENTS_STARTUP_BOOLEAN_DEFAULTS)) {
@@ -40137,9 +40377,6 @@ function warnAboutStartupRelaxations({ startupOverride, previousStartup, written
40137
40377
  }
40138
40378
  if (relaxations.length > 0) warnWithFallback(logger, `The deepagents startup override wrote ${relaxations.join(", ")} into ${filePath}, which is your global deepagents-cli config: it relaxes how much dcode does without asking, for every project on this machine, not just this one.`);
40139
40379
  if (startupOverride[STARTUP_RECENT_KEY] !== void 0) warnWithFallback(logger, `The deepagents startup override's '${STARTUP_RECENT_KEY}' was not written to ${filePath}: dcode manages that key itself, and with no explicit 'mode' beside it, it is what restores auto-approval at launch. Set 'mode' if switching approval modes is the intent.`);
40140
- const known = new Set(DEEPAGENTS_STARTUP_KEYS);
40141
- const unknownKeys = writtenKeys.filter((key) => !known.has(key));
40142
- if (unknownKeys.length > 0) warnWithFallback(logger, `The deepagents startup override wrote ${unknownKeys.join(", ")} into ${filePath} unchecked — rulesync does not know what those keys grant, and that file is your global deepagents-cli config.`);
40143
40380
  }
40144
40381
  /**
40145
40382
  * Read `[shell].allow_list` the way dcode does: a TOML array is taken element
@@ -54760,7 +54997,10 @@ var WarpSkill = class WarpSkill extends ToolSkill {
54760
54997
  //#region src/features/skills/zcode-skill.ts
54761
54998
  const ZcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
54762
54999
  name: zod_mini.z.string(),
54763
- description: zod_mini.z.string()
55000
+ description: zod_mini.z.string(),
55001
+ when_to_use: zod_mini.z.optional(zod_mini.z.string()),
55002
+ license: zod_mini.z.optional(zod_mini.z.string()),
55003
+ metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
54764
55004
  });
54765
55005
  /**
54766
55006
  * Represents a ZCode skill directory.
@@ -54823,10 +55063,16 @@ var ZcodeSkill = class ZcodeSkill extends ToolSkill {
54823
55063
  }
54824
55064
  toRulesyncSkill() {
54825
55065
  const frontmatter = this.getFrontmatter();
55066
+ const zcodeSection = {
55067
+ ...frontmatter.when_to_use !== void 0 && { when_to_use: frontmatter.when_to_use },
55068
+ ...frontmatter.license !== void 0 && { license: frontmatter.license },
55069
+ ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
55070
+ };
54826
55071
  const rulesyncFrontmatter = {
54827
55072
  name: frontmatter.name,
54828
55073
  description: frontmatter.description,
54829
- targets: ["*"]
55074
+ targets: ["*"],
55075
+ ...Object.keys(zcodeSection).length > 0 && { zcode: zcodeSection }
54830
55076
  };
54831
55077
  return new RulesyncSkill({
54832
55078
  outputRoot: this.outputRoot,
@@ -54841,9 +55087,21 @@ var ZcodeSkill = class ZcodeSkill extends ToolSkill {
54841
55087
  }
54842
55088
  static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
54843
55089
  const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
55090
+ const zcodeSection = rulesyncFrontmatter.zcode;
55091
+ const license = resolveLicense({
55092
+ rootFrontmatter: rulesyncFrontmatter,
55093
+ section: zcodeSection
55094
+ });
55095
+ const metadata = resolveMetadata({
55096
+ rootFrontmatter: rulesyncFrontmatter,
55097
+ section: zcodeSection
55098
+ });
54844
55099
  const zcodeFrontmatter = {
54845
55100
  name: rulesyncFrontmatter.name,
54846
- description: rulesyncFrontmatter.description
55101
+ description: rulesyncFrontmatter.description,
55102
+ ...zcodeSection?.when_to_use !== void 0 && { when_to_use: zcodeSection.when_to_use },
55103
+ ...license !== void 0 && { license },
55104
+ ...metadata !== void 0 && { metadata }
54847
55105
  };
54848
55106
  const settablePaths = ZcodeSkill.getSettablePaths({ global });
54849
55107
  return new ZcodeSkill({
@@ -62944,10 +63202,21 @@ var ClineRule = class ClineRule extends ToolRule {
62944
63202
  */
62945
63203
  const CodebuddyRuleFrontmatterSchema = zod_mini.z.object({
62946
63204
  description: zod_mini.z.optional(zod_mini.z.string()),
62947
- paths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
62948
- alwaysApply: zod_mini.z.optional(zod_mini.z.boolean())
63205
+ paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
63206
+ alwaysApply: zod_mini.z.optional(zod_mini.z.boolean()),
63207
+ enabled: zod_mini.z.optional(zod_mini.z.boolean())
62949
63208
  });
62950
63209
  /**
63210
+ * Normalizes the documented `string` / `string[]` shapes of `paths` to the
63211
+ * list form the rest of the adapter works with. An empty list and an empty
63212
+ * string both mean "no paths".
63213
+ */
63214
+ function normalizeCodebuddyPaths(paths) {
63215
+ if (paths === void 0) return;
63216
+ const list = (typeof paths === "string" ? [paths] : paths).filter((path) => path.trim() !== "");
63217
+ return list.length > 0 ? list : void 0;
63218
+ }
63219
+ /**
62951
63220
  * A universal glob (matching everything) is redundant on an Always Apply
62952
63221
  * rule and, paired with `alwaysApply: true`, is the same semantic conflict
62953
63222
  * `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
@@ -62965,7 +63234,7 @@ const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
62965
63234
  * Rules format:
62966
63235
  * - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
62967
63236
  * - {project}/.codebuddy/rules/*.md (root: false, with optional
62968
- * `description` / `paths` / `alwaysApply` frontmatter)
63237
+ * `description` / `paths` / `alwaysApply` / `enabled` frontmatter)
62969
63238
  * - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
62970
63239
  *
62971
63240
  * @see https://www.codebuddy.ai/docs/cli/memory
@@ -63007,9 +63276,10 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
63007
63276
  this.body = body;
63008
63277
  }
63009
63278
  static generateFileContent(body, frontmatter) {
63010
- if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
63279
+ if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0 && frontmatter.enabled === void 0) return body;
63011
63280
  return stringifyFrontmatter(body, {
63012
63281
  description: frontmatter.description,
63282
+ enabled: frontmatter.enabled,
63013
63283
  alwaysApply: frontmatter.alwaysApply,
63014
63284
  paths: frontmatter.paths
63015
63285
  });
@@ -63057,10 +63327,36 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
63057
63327
  root: isRoot
63058
63328
  });
63059
63329
  }
63060
- static resolveCodebuddyPaths({ paths, alwaysApply }) {
63061
- if (!paths || paths.length === 0) return;
63062
- if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
63063
- return paths;
63330
+ /**
63331
+ * Resolves the `paths` / `alwaysApply` pair against CodeBuddy's Rule Type
63332
+ * Determination table, which reads:
63333
+ *
63334
+ * | `alwaysApply` | `paths` | Rule type |
63335
+ * | ---------------- | --------- | ---------------------------------- |
63336
+ * | `true` (default) | any | ALWAYS — always injected |
63337
+ * | `false` | has value | MANUAL — triggered on matching file |
63338
+ * | `false` | none | not supported; the rule is dropped |
63339
+ *
63340
+ * The default being `true` is the opposite of Cursor, which the adapter was
63341
+ * modeled on: `paths` alone scopes nothing, so a rule meant to be path
63342
+ * triggered has to carry an explicit `alwaysApply: false` alongside it.
63343
+ *
63344
+ * @see https://www.codebuddy.ai/docs/cli/memory
63345
+ */
63346
+ static resolveCodebuddyRuleType({ paths, alwaysApply }) {
63347
+ const scopedPaths = paths && paths.length > 0 && !paths.every((path) => UNIVERSAL_PATHS.has(path.trim())) ? paths : void 0;
63348
+ if (alwaysApply === true) return {
63349
+ paths: scopedPaths,
63350
+ alwaysApply: true
63351
+ };
63352
+ if (scopedPaths === void 0) return {
63353
+ paths: void 0,
63354
+ alwaysApply: void 0
63355
+ };
63356
+ return {
63357
+ paths: scopedPaths,
63358
+ alwaysApply: false
63359
+ };
63064
63360
  }
63065
63361
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
63066
63362
  const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
@@ -63077,17 +63373,17 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
63077
63373
  root
63078
63374
  });
63079
63375
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
63080
- const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
63376
+ const codebuddyPaths = normalizeCodebuddyPaths(rulesyncFrontmatter.codebuddy?.paths);
63081
63377
  const globs = rulesyncFrontmatter.globs;
63082
- const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
63083
- const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
63378
+ const ruleType = CodebuddyRule.resolveCodebuddyRuleType({
63084
63379
  paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
63085
- alwaysApply: alwaysApply === true
63380
+ alwaysApply: rulesyncFrontmatter.codebuddy?.alwaysApply
63086
63381
  });
63087
63382
  const codebuddyFrontmatter = {
63088
63383
  description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
63089
- paths: pathsValue,
63090
- alwaysApply
63384
+ paths: ruleType.paths,
63385
+ alwaysApply: ruleType.alwaysApply,
63386
+ enabled: rulesyncFrontmatter.codebuddy?.enabled
63091
63387
  };
63092
63388
  return new CodebuddyRule({
63093
63389
  outputRoot,
@@ -63117,17 +63413,19 @@ var CodebuddyRule = class CodebuddyRule extends ToolRule {
63117
63413
  validate: true
63118
63414
  });
63119
63415
  }
63120
- const isAlways = this.frontmatter.alwaysApply === true;
63121
- const sourcePaths = this.frontmatter.paths ?? [];
63416
+ const sourcePaths = normalizeCodebuddyPaths(this.frontmatter.paths) ?? [];
63417
+ const isAlways = this.frontmatter.alwaysApply !== false;
63122
63418
  const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
63419
+ const alwaysApply = this.frontmatter.alwaysApply ?? (sourcePaths.length > 0 ? true : void 0);
63123
63420
  const rulesyncFrontmatter = {
63124
63421
  targets,
63125
63422
  root: false,
63126
63423
  description: this.frontmatter.description,
63127
63424
  globs,
63128
- ...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
63129
- paths: this.frontmatter.paths,
63130
- alwaysApply: this.frontmatter.alwaysApply,
63425
+ ...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.enabled !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
63426
+ paths: sourcePaths.length > 0 ? sourcePaths : void 0,
63427
+ alwaysApply,
63428
+ enabled: this.frontmatter.enabled,
63131
63429
  description: this.frontmatter.description
63132
63430
  } }
63133
63431
  };
@@ -65910,33 +66208,49 @@ var ReplitRule = class ReplitRule extends ToolRule {
65910
66208
  * Supports plain Markdown without frontmatter, mode-specific rules,
65911
66209
  * and both directory-based and single-file configurations.
65912
66210
  *
65913
- * - Project scope writes the non-root directory `.roo/rules/`.
66211
+ * - Project scope writes the non-root directory `.roo/rules/`, and the root
66212
+ * rule to the workspace-root `AGENTS.md` the extension reads from `cwd`.
65914
66213
  * - Global scope writes the same non-root directory resolved under the home
65915
- * directory (`~/.roo/rules/`), which Roo loads before workspace rules.
66214
+ * directory (`~/.roo/rules/`), which Roo loads before workspace rules. The
66215
+ * root rule joins it as `~/.roo/rules/AGENTS.md`: agent-rules discovery is
66216
+ * workspace-only, so the default project-scope root would resolve to a
66217
+ * `~/AGENTS.md` nothing ever reads.
65916
66218
  * @see https://roocodeinc.github.io/Roo-Code/features/custom-instructions
65917
66219
  */
65918
66220
  var RooRule = class RooRule extends ToolRule {
65919
- static getSettablePaths(_options = {}) {
65920
- return { nonRoot: { relativeDirPath: buildToolPath(ROO_DIR, "rules", _options.excludeToolDir) } };
66221
+ static getSettablePaths({ global, excludeToolDir } = {}) {
66222
+ const rulesDirPath = buildToolPath(ROO_DIR, "rules", excludeToolDir);
66223
+ if (global) return {
66224
+ root: {
66225
+ relativeDirPath: rulesDirPath,
66226
+ relativeFilePath: ROO_GLOBAL_ROOT_RULE_FILE_NAME
66227
+ },
66228
+ nonRoot: { relativeDirPath: rulesDirPath }
66229
+ };
66230
+ return { nonRoot: { relativeDirPath: rulesDirPath } };
65921
66231
  }
65922
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true }) {
65923
- const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : this.getSettablePaths().nonRoot.relativeDirPath;
66232
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
66233
+ const paths = this.getSettablePaths({ global });
66234
+ const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : paths.nonRoot?.relativeDirPath ?? buildToolPath(".roo", "rules");
65924
66235
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
66236
+ const isRoot = "root" in paths && relativeDirPath === paths.root.relativeDirPath && relativeFilePath === paths.root.relativeFilePath;
65925
66237
  return new RooRule({
65926
66238
  outputRoot,
65927
66239
  relativeDirPath,
65928
66240
  relativeFilePath,
65929
66241
  fileContent,
65930
66242
  validate,
65931
- root: false
66243
+ root: isRoot
65932
66244
  });
65933
66245
  }
65934
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
66246
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
66247
+ const paths = this.getSettablePaths({ global });
65935
66248
  const params = this.buildToolRuleParamsDefault({
65936
66249
  outputRoot,
65937
66250
  rulesyncRule,
65938
66251
  validate,
65939
- nonRootPath: this.getSettablePaths().nonRoot
66252
+ ..."root" in paths && { rootPath: paths.root },
66253
+ nonRootPath: paths.nonRoot
65940
66254
  });
65941
66255
  const mode = rulesyncRule.getFrontmatter().roo?.mode;
65942
66256
  if (!params.root && mode !== void 0 && mode !== "") {
@@ -66859,7 +67173,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
66859
67173
  extension: "md",
66860
67174
  supportsGlobal: true,
66861
67175
  ruleDiscoveryMode: "auto",
66862
- collisionPolicy: "fold"
67176
+ collisionPolicy: "fold",
67177
+ localRootMode: "separate-local-file",
67178
+ localRootFileName: CRUSH_LOCAL_RULE_FILE_NAME
66863
67179
  }
66864
67180
  }],
66865
67181
  ["cursor", {
@@ -67579,6 +67895,34 @@ var RulesProcessor = class extends FeatureProcessor {
67579
67895
  root: true,
67580
67896
  localRoot
67581
67897
  });
67898
+ if (isClassOrSubclassOf({
67899
+ candidate: factory.class,
67900
+ base: CodebuddyRule
67901
+ })) {
67902
+ const paths = CodebuddyRule.getSettablePaths({ global: this.global });
67903
+ return new CodebuddyRule({
67904
+ outputRoot: this.outputRoot,
67905
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
67906
+ relativeFilePath: fileName,
67907
+ frontmatter: {},
67908
+ body,
67909
+ validate: true,
67910
+ root: true,
67911
+ localRoot
67912
+ });
67913
+ }
67914
+ if (isClassOrSubclassOf({
67915
+ candidate: factory.class,
67916
+ base: CrushRule
67917
+ })) return new CrushRule({
67918
+ outputRoot: this.outputRoot,
67919
+ relativeDirPath: relativeDirPath ?? ".",
67920
+ relativeFilePath: fileName,
67921
+ fileContent: body,
67922
+ validate: true,
67923
+ root: true,
67924
+ localRoot
67925
+ });
67582
67926
  if (isClassOrSubclassOf({
67583
67927
  candidate: factory.class,
67584
67928
  base: QwencodeRule
@@ -70279,6 +70623,12 @@ Object.defineProperty(exports, "CONFLICTING_TARGET_PAIRS", {
70279
70623
  return CONFLICTING_TARGET_PAIRS;
70280
70624
  }
70281
70625
  });
70626
+ Object.defineProperty(exports, "CRUSH_LOCAL_RULE_FILE_NAME", {
70627
+ enumerable: true,
70628
+ get: function() {
70629
+ return CRUSH_LOCAL_RULE_FILE_NAME;
70630
+ }
70631
+ });
70282
70632
  Object.defineProperty(exports, "CURATED_RULES_FEATURE_SUBDIR", {
70283
70633
  enumerable: true,
70284
70634
  get: function() {