rulesync 16.36.0 → 16.38.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.
@@ -591,6 +591,7 @@ const mcpProcessorToolTargetTuple = [
591
591
  "junie",
592
592
  "musecode",
593
593
  "opencode",
594
+ "pool",
594
595
  "qwencode",
595
596
  "reasonix",
596
597
  "roo",
@@ -3304,6 +3305,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3304
3305
  ".grok/config.toml",
3305
3306
  ".rovodev/config.yml",
3306
3307
  ".rovodev/mcp.json",
3308
+ ".poolside/settings.yaml",
3309
+ ".config/poolside/settings.yaml",
3307
3310
  ".tabnine/agent/settings.json",
3308
3311
  ".vibe/config.toml",
3309
3312
  ".zcode/config.json",
@@ -4353,7 +4356,7 @@ const PI_HOOK_EVENTS = [
4353
4356
  * adapter. Amp's `agent.start` / `agent.end` events describe the main agent
4354
4357
  * turn, not a subagent lifecycle, so they map to `beforeSubmitPrompt` / `stop`.
4355
4358
  *
4356
- * @see https://ampcode.com/manual/plugin-api
4359
+ * @see https://ampcode.com/docs/plugin-api
4357
4360
  */
4358
4361
  const AMP_HOOK_EVENTS = [
4359
4362
  "sessionStart",
@@ -6198,6 +6201,7 @@ const McpServerSchema = z.looseObject({
6198
6201
  crushOauthClientSecret: z.optional(z.string()),
6199
6202
  crushOauthCallbackPort: z.optional(z.int()),
6200
6203
  crushSessionless: z.optional(z.boolean()),
6204
+ poolAllow: z.optional(z.array(z.string())),
6201
6205
  headers: z.optional(z.record(z.string(), z.string())),
6202
6206
  /**
6203
6207
  * The canonical per-server tool allowlist.
@@ -6629,6 +6633,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
6629
6633
  "crushOauthClientSecret",
6630
6634
  "crushOauthCallbackPort",
6631
6635
  "crushSessionless",
6636
+ "poolAllow",
6632
6637
  "oauth_client_id",
6633
6638
  "oauth_client_secret",
6634
6639
  "oauth_callback_port",
@@ -8316,7 +8321,7 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
8316
8321
  * them; treating a segment that carries one as static could name a directory
8317
8322
  * that no matched file actually lives under.
8318
8323
  */
8319
- const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
8324
+ const GLOB_METACHARACTERS$1 = /[*?[\]{}()!]/;
8320
8325
  /**
8321
8326
  * The directory every file matched by `glob` lives under, or `undefined` when
8322
8327
  * the pattern pins down no such directory.
@@ -8349,7 +8354,7 @@ function getGlobStaticPrefix(glob) {
8349
8354
  const lastIndex = segments.length - 1;
8350
8355
  const prefix = [];
8351
8356
  for (const [index, segment] of segments.entries()) {
8352
- if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
8357
+ if (segment === "" || GLOB_METACHARACTERS$1.test(segment)) break;
8353
8358
  if (segment === ".") continue;
8354
8359
  if (index === lastIndex) break;
8355
8360
  prefix.push(segment);
@@ -8420,6 +8425,7 @@ const RulesyncRuleFrontmatterSchema = z.object({
8420
8425
  trigger: z.optional(z.string()),
8421
8426
  globs: z.optional(z.array(z.string()))
8422
8427
  })),
8428
+ aiassistant: z.optional(z.looseObject({ apply: z.optional(z.string()) })),
8423
8429
  devin: z.optional(z.looseObject({
8424
8430
  trigger: z.optional(z.string()),
8425
8431
  globs: z.optional(z.array(z.string())),
@@ -9644,6 +9650,8 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
9644
9650
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
9645
9651
  "disable-model-invocation": z.optional(z.boolean()),
9646
9652
  "user-invocable": z.optional(z.boolean()),
9653
+ icon: z.optional(z.string()),
9654
+ color: z.optional(z.string()),
9647
9655
  metadata: z.optional(z.looseObject({}))
9648
9656
  })),
9649
9657
  factorydroid: z.optional(z.looseObject({
@@ -12921,6 +12929,8 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
12921
12929
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
12922
12930
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
12923
12931
  const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
12932
+ const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
12933
+ const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
12924
12934
  /**
12925
12935
  * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
12926
12936
  * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
@@ -13002,6 +13012,20 @@ const KIMI_CODE_CONFIG_DECLARATION = {
13002
13012
  }
13003
13013
  };
13004
13014
  /**
13015
+ * Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
13016
+ * the user's primary Pool settings file, so an unparseable root fails closed
13017
+ * rather than being replaced with generated output. `mcp_servers` is owned as
13018
+ * a whole key because the MCP writer emits the complete server map.
13019
+ */
13020
+ const POOL_SETTINGS_DECLARATION = {
13021
+ format: "yaml",
13022
+ invalidRootPolicy: "error",
13023
+ features: { mcp: {
13024
+ kind: "replace-owned-keys",
13025
+ ownedKeys: ["mcp_servers"]
13026
+ } }
13027
+ };
13028
+ /**
13005
13029
  * ZCode's settings file, which also carries model/theme/permission keys
13006
13030
  * rulesync does not own. Both copies are the user's primary ZCode config, so
13007
13031
  * every writer refuses to read-modify-write a file it could not parse rather
@@ -13106,6 +13130,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13106
13130
  [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13107
13131
  [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13108
13132
  [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13133
+ [POOL_PROJECT_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
13134
+ [POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
13109
13135
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
13110
13136
  format: "yaml",
13111
13137
  invalidRootPolicy: "error",
@@ -23658,14 +23684,23 @@ var ContinueHooks = class extends SettingsJsonHooks {
23658
23684
  * portable `command` field is written, which upstream copies to both when
23659
23685
  * neither is present. Note the cloud agent runs hooks in a Linux sandbox and
23660
23686
  * honors only `bash` and `command` — a `powershell` entry is ignored there.
23687
+ * VS Code reads the same file and maps `bash` to `osx` / `linux` and
23688
+ * `powershell` to `windows`; it also documents its own per-OS overrides
23689
+ * (`windows`, `linux`, `osx`) next to `command`, which are typed here so an
23690
+ * entry that uses them instead of a shell field is not mistaken for one
23691
+ * without a command.
23661
23692
  *
23662
23693
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
23694
+ * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
23663
23695
  */
23664
23696
  const CopilotHookEntrySchema = z.looseObject({
23665
23697
  type: z.string(),
23666
23698
  bash: z.optional(z.string()),
23667
23699
  powershell: z.optional(z.string()),
23668
23700
  command: z.optional(z.string()),
23701
+ windows: z.optional(z.string()),
23702
+ linux: z.optional(z.string()),
23703
+ osx: z.optional(z.string()),
23669
23704
  cwd: z.optional(z.string()),
23670
23705
  env: z.optional(z.record(z.string(), z.string())),
23671
23706
  timeoutSec: z.optional(z.number()),
@@ -23756,17 +23791,30 @@ function resolveImportCommand$1(entry, logger) {
23756
23791
  *
23757
23792
  * Generate re-emits any non-canonical key verbatim through `rest`, so a key
23758
23793
  * dropped here does not survive an import → generate round trip. `cwd` is a
23759
- * documented Copilot hook field and was previously lost that way.
23794
+ * documented Copilot hook field and was previously lost that way, as were the
23795
+ * VS Code per-OS overrides `windows` / `linux` / `osx`: an entry authored with
23796
+ * only those (no `command`) imported as a hook with no command at all and was
23797
+ * regenerated empty.
23760
23798
  *
23761
23799
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
23762
23800
  * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
23801
+ * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
23763
23802
  */
23764
23803
  function importPassthrough$1(entry) {
23765
23804
  const passthrough = {};
23766
23805
  if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
23767
23806
  if (entry.env !== void 0) passthrough.env = entry.env;
23807
+ for (const key of OS_OVERRIDE_KEYS) if (entry[key] !== void 0) passthrough[key] = entry[key];
23768
23808
  return passthrough;
23769
23809
  }
23810
+ const OS_OVERRIDE_KEYS = [
23811
+ "windows",
23812
+ "linux",
23813
+ "osx"
23814
+ ];
23815
+ function hasOsOverride(passthrough) {
23816
+ return OS_OVERRIDE_KEYS.some((key) => passthrough[key] !== void 0);
23817
+ }
23770
23818
  /**
23771
23819
  * Extract hooks from Copilot hooks JSON into canonical format.
23772
23820
  * Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
@@ -23787,12 +23835,14 @@ function copilotHooksToCanonical(copilotHooks, logger) {
23787
23835
  const entry = parseResult.data;
23788
23836
  const { command, shell } = resolveImportCommand$1(entry, logger);
23789
23837
  const timeout = entry.timeoutSec ?? entry.timeout;
23838
+ const passthrough = importPassthrough$1(entry);
23839
+ if (command === void 0 && hasOsOverride(passthrough)) logger?.warn(`Copilot hook on '${copilotEventName}' has only VS Code per-OS overrides (windows/linux/osx) and no portable command; it is preserved for copilot, but other targets will generate it without a command until a \`command\` is added.`);
23790
23840
  defs.push({
23791
23841
  type: "command",
23792
23842
  ...command !== void 0 && { command },
23793
23843
  ...shell !== void 0 && { shell },
23794
23844
  ...timeout !== void 0 && { timeout },
23795
- ...importPassthrough$1(entry)
23845
+ ...passthrough
23796
23846
  });
23797
23847
  }
23798
23848
  if (defs.length > 0) canonical[eventName] = defs;
@@ -23955,6 +24005,8 @@ const CopilotCliHookEntrySchema = z.looseObject({
23955
24005
  bash: z.optional(z.string()),
23956
24006
  powershell: z.optional(z.string()),
23957
24007
  command: z.optional(z.string()),
24008
+ exec: z.optional(z.string()),
24009
+ args: z.optional(z.array(z.string())),
23958
24010
  prompt: z.optional(z.string()),
23959
24011
  url: z.optional(z.string()),
23960
24012
  headers: z.optional(z.record(z.string(), z.string())),
@@ -23984,6 +24036,32 @@ function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger
23984
24036
  return {};
23985
24037
  }
23986
24038
  /**
24039
+ * Pick the command fields of an exported `command` entry.
24040
+ *
24041
+ * A canonical `args` list (an empty one counts) selects the CLI's shell-free
24042
+ * form: `command` becomes `exec` and the list is written as `args`, so the
24043
+ * executable is spawned directly. The `shell` selector is meaningless there
24044
+ * and is dropped with a warning. Otherwise the canonical `shell` selector
24045
+ * picks the shell-specific field; without it the portable `command` field is
24046
+ * written, which upstream copies to both `bash` and `powershell`. Keying off
24047
+ * `shell` rather than `process.platform` keeps the generated file identical
24048
+ * on every machine.
24049
+ */
24050
+ function resolveExportCommandPart({ def, eventName, logger }) {
24051
+ if (Array.isArray(def.args)) {
24052
+ if (def.shell !== void 0) logger?.warn(`Copilot CLI runs an \`exec\` hook without a shell; dropping shell "${def.shell}" on '${eventName}'.`);
24053
+ return compact({
24054
+ exec: def.command,
24055
+ args: def.args,
24056
+ env: def.env
24057
+ });
24058
+ }
24059
+ return compact({
24060
+ [def.shell ?? "command"]: def.command,
24061
+ env: def.env
24062
+ });
24063
+ }
24064
+ /**
23987
24065
  * Build the exported entries for a single canonical event. Returns an empty
23988
24066
  * array when no entries are emitted (e.g. all prompt hooks were skipped).
23989
24067
  */
@@ -24023,19 +24101,17 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
24023
24101
  ...timeoutPart,
24024
24102
  ...rest
24025
24103
  });
24026
- else if (hookType === "command") {
24027
- const commandField = def.shell ?? "command";
24028
- entries.push({
24029
- type: "command",
24030
- ...matcherPart,
24031
- ...compact({
24032
- [commandField]: def.command,
24033
- env: def.env
24034
- }),
24035
- ...timeoutPart,
24036
- ...rest
24037
- });
24038
- }
24104
+ else if (hookType === "command") entries.push({
24105
+ type: "command",
24106
+ ...matcherPart,
24107
+ ...resolveExportCommandPart({
24108
+ def,
24109
+ eventName,
24110
+ logger
24111
+ }),
24112
+ ...timeoutPart,
24113
+ ...rest
24114
+ });
24039
24115
  }
24040
24116
  return entries;
24041
24117
  }
@@ -24075,6 +24151,10 @@ function importPassthrough(entry) {
24075
24151
  /**
24076
24152
  * Resolve the canonical command and its `shell` selector from an imported entry.
24077
24153
  *
24154
+ * The CLI-only `exec` form maps to canonical `command` + `args`, the exec form
24155
+ * shared with AugmentCode and Claude Code, so it no longer imports as a hook
24156
+ * with no command at all.
24157
+ *
24078
24158
  * A shell-specific field carries its `shell` through so re-export writes the
24079
24159
  * same field back. An entry using only the portable `command` field leaves
24080
24160
  * `shell` unset, which re-export renders as the portable field again.
@@ -24088,6 +24168,13 @@ function importPassthrough(entry) {
24088
24168
  function resolveImportCommand(entry, logger) {
24089
24169
  const hasBash = typeof entry.bash === "string";
24090
24170
  const hasPowershell = typeof entry.powershell === "string";
24171
+ if (typeof entry.exec === "string") {
24172
+ if (hasBash || hasPowershell || typeof entry.command === "string") logger?.warn("Copilot CLI hook has both exec and a shell command; using exec and ignoring the shell fields, as the hooks reference says not to combine them.");
24173
+ return {
24174
+ command: entry.exec,
24175
+ args: entry.args ?? []
24176
+ };
24177
+ }
24091
24178
  if (hasBash && hasPowershell) {
24092
24179
  logger?.warn("Copilot CLI hook has both bash and powershell commands; using bash and ignoring powershell, so the imported config does not depend on the machine the import ran on.");
24093
24180
  return {
@@ -24137,11 +24224,12 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
24137
24224
  ...passthrough
24138
24225
  });
24139
24226
  else {
24140
- const { command, shell } = resolveImportCommand(entry, logger);
24227
+ const { command, shell, args } = resolveImportCommand(entry, logger);
24141
24228
  defs.push({
24142
24229
  type: "command",
24143
24230
  ...command !== void 0 && { command },
24144
24231
  ...shell !== void 0 && { shell },
24232
+ ...args !== void 0 && { args },
24145
24233
  ...matcherPart,
24146
24234
  ...timeoutPart,
24147
24235
  ...passthrough
@@ -24691,6 +24779,7 @@ var CursorHooks = class CursorHooks extends ToolHooks {
24691
24779
  ...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
24692
24780
  ...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher },
24693
24781
  ...def.prompt !== void 0 && def.prompt !== null && { prompt: def.prompt },
24782
+ ...def.type === "prompt" && def.model !== void 0 && def.model !== null && { model: def.model },
24694
24783
  ...def.failClosed !== void 0 && def.failClosed !== null && { failClosed: def.failClosed }
24695
24784
  }));
24696
24785
  if (mappedDefs.length > 0) mappedHooks[cursorEventName] = mappedDefs;
@@ -37028,6 +37117,363 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
37028
37117
  }
37029
37118
  };
37030
37119
  //#endregion
37120
+ //#region src/constants/pool-paths.ts
37121
+ /**
37122
+ * Pool (Poolside's coding agent CLI) configuration-layout conventions.
37123
+ *
37124
+ * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
37125
+ * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
37126
+ * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
37127
+ * path), the project-root `AGENTS.md`, and nested per-directory
37128
+ * `AGENTS.md` files from the repository root down through the working
37129
+ * directory, deeper files taking precedence. It skips ignored directories
37130
+ * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
37131
+ *
37132
+ * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
37133
+ * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
37134
+ * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
37135
+ * the skill directories of other Agent Skills tools; rulesync writes only the
37136
+ * Pool-specific roots and leaves the shared ones to their own targets.
37137
+ *
37138
+ * @see https://docs.poolside.ai/agent-instructions
37139
+ * @see https://docs.poolside.ai/skills
37140
+ * @see https://github.com/poolsideai/pool
37141
+ */
37142
+ /** Project-scoped `.poolside/` directory at the project root. */
37143
+ const POOL_DIR = ".poolside";
37144
+ /** Global config directory for Pool, relative to the home directory. */
37145
+ const POOL_GLOBAL_DIR = join(".config", "poolside");
37146
+ /** Project skills root, relative to the project root. */
37147
+ const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
37148
+ /** Global skills root, relative to the home directory. */
37149
+ const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
37150
+ /**
37151
+ * Pool's settings file name. MCP servers live under the top-level
37152
+ * `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
37153
+ * `~/.config/poolside/settings.yaml` (global); the untracked
37154
+ * `.poolside/settings.local.yaml` overlay is left to the user.
37155
+ *
37156
+ * @see https://docs.poolside.ai/mcp-servers
37157
+ * @see https://docs.poolside.ai/settings-file-reference
37158
+ */
37159
+ const POOL_SETTINGS_FILE_NAME = "settings.yaml";
37160
+ /** Top-level key of Pool's settings file that holds the MCP server map. */
37161
+ const POOL_MCP_SERVERS_KEY = "mcp_servers";
37162
+ //#endregion
37163
+ //#region src/features/mcp/pool-mcp.ts
37164
+ /**
37165
+ * Single spelling of the settings.yaml codec/policy, matching the
37166
+ * `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
37167
+ * unparseable root rather than replacing the user's primary Pool settings with
37168
+ * generated output.
37169
+ */
37170
+ function parsePoolSettings(fileContent, filePath) {
37171
+ return parseSharedConfig({
37172
+ format: "yaml",
37173
+ fileContent,
37174
+ filePath,
37175
+ invalidRootPolicy: "error"
37176
+ });
37177
+ }
37178
+ /**
37179
+ * The remote transports Pool's `transport.type` accepts. `streamable-http` is
37180
+ * the canonical rulesync name for what Pool calls `http`; everything else is
37181
+ * left to the caller to reject.
37182
+ *
37183
+ * @see https://docs.poolside.ai/mcp-servers
37184
+ */
37185
+ function asPoolRemoteType(stated, url) {
37186
+ if (stated === "sse") return "sse";
37187
+ if (stated === "http" || stated === "streamable-http") return "http";
37188
+ if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
37189
+ }
37190
+ /**
37191
+ * Pool spells remote headers as a list of `"Name: value"` strings rather than
37192
+ * a map, so the canonical record is flattened at generate time and split back
37193
+ * at the first `:` on import. A list entry without a `:` names no header and
37194
+ * is dropped with a warning — silently losing what may be a mistyped auth
37195
+ * header would leave the canonical file short of one without a trace.
37196
+ */
37197
+ function headersRecordToPoolList(headers) {
37198
+ return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
37199
+ }
37200
+ function poolHeadersListToRecord(serverName, headers) {
37201
+ if (!isStringArray$2(headers)) return void 0;
37202
+ const result = {};
37203
+ for (const entry of headers) {
37204
+ const separator = entry.indexOf(":");
37205
+ const name = separator > 0 ? entry.slice(0, separator).trim() : "";
37206
+ if (name === "" || PROTOTYPE_POLLUTION_KEYS.has(name)) {
37207
+ warnWithFallback(void 0, `Ignored malformed header ${quoteValueForWarning(entry)} in Pool MCP server ${quoteValueForWarning(serverName)}: expected "Name: value"`);
37208
+ continue;
37209
+ }
37210
+ result[name] = entry.slice(separator + 1).trim();
37211
+ }
37212
+ return Object.keys(result).length > 0 ? result : void 0;
37213
+ }
37214
+ /**
37215
+ * Pool's `deny`/`enabled_tools` entries are glob patterns, while the canonical
37216
+ * `enabledTools`/`disabledTools` lists are literal tool names that the other
37217
+ * targets match verbatim. A pattern therefore imports as a name that matches
37218
+ * nothing elsewhere — a `deny` that quietly stops denying is the case worth
37219
+ * calling out — so it is copied as-is but warned about. `allow` is exempt: it
37220
+ * imports to the Pool-only `poolAllow`, where globs are exactly what Pool
37221
+ * expects and no other tool ever reads them.
37222
+ */
37223
+ const GLOB_METACHARACTERS = /[*?[]/;
37224
+ /** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
37225
+ const MAX_LISTED_GLOB_PATTERNS = 10;
37226
+ function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
37227
+ if (!isStringArray$2(value)) {
37228
+ warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
37229
+ return;
37230
+ }
37231
+ if (!warnOnGlobs) return value;
37232
+ const patterns = value.filter((entry) => GLOB_METACHARACTERS.test(entry));
37233
+ if (patterns.length > 0) {
37234
+ const listed = patterns.slice(0, MAX_LISTED_GLOB_PATTERNS).map(quoteValueForWarning);
37235
+ const overflow = patterns.length - listed.length;
37236
+ const shown = overflow > 0 ? `${listed.join(", ")} (+${overflow} more)` : listed.join(", ");
37237
+ warnWithFallback(void 0, `${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)} contains glob patterns (${shown}); other tools match the imported tool names literally`);
37238
+ }
37239
+ return value;
37240
+ }
37241
+ /**
37242
+ * Pool's `transport` block for a remote server, or `null` (after a warning)
37243
+ * when the server has no url or names a transport Pool cannot reach.
37244
+ */
37245
+ function toPoolTransport(name, config, logger) {
37246
+ const url = resolveRemoteMcpUrl(config);
37247
+ if (!url) return warnAndSkipMcpServer({
37248
+ toolName: "Pool",
37249
+ serverName: name,
37250
+ reason: "a remote transport without a url",
37251
+ logger
37252
+ });
37253
+ const stated = config.type ?? config.transport;
37254
+ const type = asPoolRemoteType(stated, url);
37255
+ if (type === void 0) return warnAndSkipMcpServer({
37256
+ toolName: "Pool",
37257
+ serverName: name,
37258
+ reason: stated === void 0 ? "a WebSocket url, which Pool's remote transports (http and sse) cannot reach" : `the "${stated}" transport, which Pool does not offer for remote servers (only http and sse)`,
37259
+ logger
37260
+ });
37261
+ const transport = {
37262
+ type,
37263
+ url
37264
+ };
37265
+ if (config.headers && Object.keys(config.headers).length > 0) transport.headers = headersRecordToPoolList(config.headers);
37266
+ return transport;
37267
+ }
37268
+ /**
37269
+ * The transport-specific part of a Pool server entry: a `transport` block for
37270
+ * remote servers, `command`/`args`/`cwd` for stdio ones. `null` (after a
37271
+ * warning) when the server cannot be written.
37272
+ */
37273
+ function toPoolTransportFields(name, config, logger) {
37274
+ if (isRemoteMcpServer(config)) {
37275
+ const transport = toPoolTransport(name, config, logger);
37276
+ return transport === null ? null : { transport };
37277
+ }
37278
+ const [command, ...args] = resolveLocalMcpCommand(config);
37279
+ if (!command) return warnAndSkipMcpServer({
37280
+ toolName: "Pool",
37281
+ serverName: name,
37282
+ reason: "a stdio transport without a command",
37283
+ logger
37284
+ });
37285
+ const fields = {
37286
+ command,
37287
+ args
37288
+ };
37289
+ if (config.cwd) fields.cwd = config.cwd;
37290
+ return fields;
37291
+ }
37292
+ /**
37293
+ * The fields shared by stdio and remote entries. Tool filters map onto Pool's
37294
+ * own switches — `enabledTools` becomes `enabled_tools` (the tools Pool
37295
+ * exposes at all) and `disabledTools` becomes `deny` (patterns agents may
37296
+ * never use); the Pool-only approval allowlist is authored as `poolAllow` and
37297
+ * written as `allow`. `disabled: true` is spelled the same on both sides.
37298
+ */
37299
+ function toPoolCommonFields(config) {
37300
+ const fields = {};
37301
+ if (config.env && Object.keys(config.env).length > 0) fields.env = config.env;
37302
+ if (config.enabledTools && config.enabledTools.length > 0) fields.enabled_tools = config.enabledTools;
37303
+ if (config.poolAllow && config.poolAllow.length > 0) fields.allow = config.poolAllow;
37304
+ if (config.disabledTools && config.disabledTools.length > 0) fields.deny = config.disabledTools;
37305
+ if (config.disabled === true) fields.disabled = true;
37306
+ return fields;
37307
+ }
37308
+ /**
37309
+ * Convert canonical rulesync servers to Pool's native `mcp_servers` shape:
37310
+ * stdio servers carry `command`/`args`/`cwd`/`env`, remote servers carry a
37311
+ * `transport` block (`type` of `http` or `sse`, `url`, optional `headers`
37312
+ * list) plus optional `env`; see `toPoolCommonFields` for the tool filters.
37313
+ *
37314
+ * @see https://docs.poolside.ai/mcp-servers
37315
+ */
37316
+ function convertToPoolFormat(mcpServers, logger) {
37317
+ const result = {};
37318
+ for (const [name, config] of Object.entries(mcpServers)) {
37319
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
37320
+ if (!isRecord$1(config)) continue;
37321
+ if (declaresNoTransport(config)) {
37322
+ warnAndSkipMcpServer({
37323
+ toolName: "Pool",
37324
+ serverName: name,
37325
+ reason: "no transport",
37326
+ logger
37327
+ });
37328
+ continue;
37329
+ }
37330
+ const transportFields = toPoolTransportFields(name, config, logger);
37331
+ if (transportFields === null) continue;
37332
+ result[name] = {
37333
+ ...transportFields,
37334
+ ...toPoolCommonFields(config)
37335
+ };
37336
+ }
37337
+ return result;
37338
+ }
37339
+ const POOL_TO_CANONICAL_TOOL_LIST_KEYS = {
37340
+ enabled_tools: "enabledTools",
37341
+ deny: "disabledTools",
37342
+ allow: "poolAllow"
37343
+ };
37344
+ /**
37345
+ * Convert Pool's native `mcp_servers` shape back to canonical rulesync
37346
+ * servers: the `transport` block is flattened to `type`/`url`/`headers`,
37347
+ * `enabled_tools` maps back to `enabledTools`, `deny` to `disabledTools` and
37348
+ * `allow` to the namespaced `poolAllow` (see `McpServerSchema`). Unknown keys
37349
+ * pass through untouched so an import keeps whatever a hand-authored entry
37350
+ * declared; generation back out is a whitelist, so only the keys
37351
+ * `convertToPoolFormat` writes are re-emitted.
37352
+ */
37353
+ function convertFromPoolFormat(poolServers) {
37354
+ const result = {};
37355
+ for (const [name, config] of Object.entries(poolServers)) {
37356
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
37357
+ const converted = {};
37358
+ for (const [key, value] of Object.entries(config)) {
37359
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
37360
+ if (key === "transport") {
37361
+ if (!isRecord$1(value)) continue;
37362
+ if (typeof value.type === "string") converted.type = value.type;
37363
+ if (typeof value.url === "string") converted.url = value.url;
37364
+ const headers = poolHeadersListToRecord(name, value.headers);
37365
+ if (headers) converted.headers = headers;
37366
+ continue;
37367
+ }
37368
+ const canonicalKey = Object.hasOwn(POOL_TO_CANONICAL_TOOL_LIST_KEYS, key) ? POOL_TO_CANONICAL_TOOL_LIST_KEYS[key] : void 0;
37369
+ if (canonicalKey) {
37370
+ const list = importPoolToolList({
37371
+ serverName: name,
37372
+ poolKey: key,
37373
+ value,
37374
+ warnOnGlobs: canonicalKey !== "poolAllow"
37375
+ });
37376
+ if (list) converted[canonicalKey] = list;
37377
+ continue;
37378
+ }
37379
+ converted[key] = value;
37380
+ }
37381
+ result[name] = converted;
37382
+ }
37383
+ return result;
37384
+ }
37385
+ /**
37386
+ * Pool MCP servers.
37387
+ *
37388
+ * Pool reads MCP servers from the `mcp_servers` block of its settings file:
37389
+ * `<project>/.poolside/settings.yaml` at project scope (shared and committed)
37390
+ * and `~/.config/poolside/settings.yaml` at user scope. The untracked
37391
+ * `.poolside/settings.local.yaml` overlay, which Pool layers on top of both,
37392
+ * is left to the user. Every other top-level settings key is preserved and
37393
+ * the file is never deleted.
37394
+ *
37395
+ * @see https://docs.poolside.ai/mcp-servers
37396
+ * @see https://docs.poolside.ai/settings-file-reference
37397
+ */
37398
+ var PoolMcp = class PoolMcp extends ToolMcp {
37399
+ settings;
37400
+ constructor(params) {
37401
+ super(params);
37402
+ this.settings = parsePoolSettings(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
37403
+ }
37404
+ getSettings() {
37405
+ return this.settings;
37406
+ }
37407
+ isDeletable() {
37408
+ return false;
37409
+ }
37410
+ static getSettablePaths({ global = false } = {}) {
37411
+ return {
37412
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
37413
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
37414
+ };
37415
+ }
37416
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
37417
+ const paths = this.getSettablePaths({ global });
37418
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
37419
+ return new PoolMcp({
37420
+ outputRoot,
37421
+ relativeDirPath: paths.relativeDirPath,
37422
+ relativeFilePath: paths.relativeFilePath,
37423
+ fileContent,
37424
+ validate,
37425
+ global
37426
+ });
37427
+ }
37428
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
37429
+ const paths = this.getSettablePaths({ global });
37430
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
37431
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
37432
+ const converted = convertToPoolFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
37433
+ const rawServer = rulesyncMcp.getRawMcpServer(serverName);
37434
+ const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
37435
+ return [serverName, {
37436
+ ...serverConfig,
37437
+ ...isStringArray$2(poolAllow) && { poolAllow }
37438
+ }];
37439
+ })), logger);
37440
+ return new PoolMcp({
37441
+ outputRoot,
37442
+ relativeDirPath: paths.relativeDirPath,
37443
+ relativeFilePath: paths.relativeFilePath,
37444
+ fileContent: applySharedConfigPatch({
37445
+ fileKey: sharedConfigFileKey(paths),
37446
+ feature: "mcp",
37447
+ existingContent,
37448
+ patch: { [POOL_MCP_SERVERS_KEY]: converted },
37449
+ filePath
37450
+ }),
37451
+ validate,
37452
+ global
37453
+ });
37454
+ }
37455
+ toRulesyncMcp() {
37456
+ const converted = convertFromPoolFormat(isRecord$1(this.settings["mcp_servers"]) ? this.settings[POOL_MCP_SERVERS_KEY] : {});
37457
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
37458
+ }
37459
+ validate() {
37460
+ return {
37461
+ success: true,
37462
+ error: null
37463
+ };
37464
+ }
37465
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
37466
+ return new PoolMcp({
37467
+ outputRoot,
37468
+ relativeDirPath,
37469
+ relativeFilePath,
37470
+ fileContent: "",
37471
+ validate: false,
37472
+ global
37473
+ });
37474
+ }
37475
+ };
37476
+ //#endregion
37031
37477
  //#region src/features/mcp/qwencode-mcp.ts
37032
37478
  /**
37033
37479
  * Map rulesync's canonical per-server tool filters to Qwen Code's field names.
@@ -39475,6 +39921,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
39475
39921
  supportsDisabledTools: true
39476
39922
  }
39477
39923
  }],
39924
+ ["pool", {
39925
+ class: PoolMcp,
39926
+ meta: {
39927
+ supportsProject: true,
39928
+ supportsGlobal: true,
39929
+ supportsEnabledTools: true,
39930
+ supportsDisabledTools: true
39931
+ }
39932
+ }],
39478
39933
  ["qwencode", {
39479
39934
  class: QwencodeMcp,
39480
39935
  meta: {
@@ -56696,7 +57151,7 @@ const AmpSkillFrontmatterSchema = z.looseObject({
56696
57151
  * Amp natively reads Agent Skills following the open standard: a directory with a
56697
57152
  * `SKILL.md` carrying `name`/`description` frontmatter, scoped to the project
56698
57153
  * (`.agents/skills/`) and user-wide (`~/.config/agents/skills/`).
56699
- * @see https://ampcode.com/manual
57154
+ * @see https://ampcode.com/docs/customize/skills
56700
57155
  */
56701
57156
  var AmpSkill = class AmpSkill extends ToolSkill {
56702
57157
  constructor({ outputRoot = process.cwd(), relativeDirPath = AMP_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -59175,8 +59630,11 @@ const CursorSkillFrontmatterSchema = z.looseObject({
59175
59630
  name: z.string(),
59176
59631
  description: z.string(),
59177
59632
  paths: z.optional(z.union([z.string(), z.array(z.string())])),
59633
+ globs: z.optional(z.union([z.string(), z.array(z.string())])),
59178
59634
  "disable-model-invocation": z.optional(z.boolean()),
59179
59635
  "user-invocable": z.optional(z.boolean()),
59636
+ icon: z.optional(z.string()),
59637
+ color: z.optional(z.string()),
59180
59638
  metadata: z.optional(z.looseObject({}))
59181
59639
  });
59182
59640
  /**
@@ -59228,10 +59686,13 @@ var CursorSkill = class CursorSkill extends ToolSkill {
59228
59686
  }
59229
59687
  toRulesyncSkill() {
59230
59688
  const frontmatter = this.getFrontmatter();
59689
+ const paths = frontmatter.paths ?? frontmatter.globs;
59231
59690
  const cursorSection = {
59232
- ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
59691
+ ...paths !== void 0 && { paths },
59233
59692
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
59234
59693
  ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
59694
+ ...frontmatter.icon !== void 0 && { icon: frontmatter.icon },
59695
+ ...frontmatter.color !== void 0 && { color: frontmatter.color },
59235
59696
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
59236
59697
  };
59237
59698
  const rulesyncFrontmatter = {
@@ -59273,6 +59734,8 @@ var CursorSkill = class CursorSkill extends ToolSkill {
59273
59734
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
59274
59735
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
59275
59736
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
59737
+ ...cursorSection?.icon !== void 0 && { icon: cursorSection.icon },
59738
+ ...cursorSection?.color !== void 0 && { color: cursorSection.color },
59276
59739
  ...metadata !== void 0 && { metadata }
59277
59740
  };
59278
59741
  return new CursorSkill({
@@ -61657,37 +62120,6 @@ var PiSkill = class PiSkill extends ToolSkill {
61657
62120
  }
61658
62121
  };
61659
62122
  //#endregion
61660
- //#region src/constants/pool-paths.ts
61661
- /**
61662
- * Pool (Poolside's coding agent CLI) configuration-layout conventions.
61663
- *
61664
- * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
61665
- * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
61666
- * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
61667
- * path), the project-root `AGENTS.md`, and nested per-directory
61668
- * `AGENTS.md` files from the repository root down through the working
61669
- * directory, deeper files taking precedence. It skips ignored directories
61670
- * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
61671
- *
61672
- * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
61673
- * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
61674
- * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
61675
- * the skill directories of other Agent Skills tools; rulesync writes only the
61676
- * Pool-specific roots and leaves the shared ones to their own targets.
61677
- *
61678
- * @see https://docs.poolside.ai/agent-instructions
61679
- * @see https://docs.poolside.ai/skills
61680
- * @see https://github.com/poolsideai/pool
61681
- */
61682
- /** Project-scoped `.poolside/` directory at the project root. */
61683
- const POOL_DIR = ".poolside";
61684
- /** Global config directory for Pool, relative to the home directory. */
61685
- const POOL_GLOBAL_DIR = join(".config", "poolside");
61686
- /** Project skills root, relative to the project root. */
61687
- const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
61688
- /** Global skills root, relative to the home directory. */
61689
- const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
61690
- //#endregion
61691
62123
  //#region src/features/skills/pool-skill.ts
61692
62124
  const PoolSkillFrontmatterSchema = z.looseObject({
61693
62125
  name: z.string(),
@@ -66032,11 +66464,17 @@ var CopilotSubagent = class CopilotSubagent extends ToolSubagent {
66032
66464
  *
66033
66465
  * Reference: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli
66034
66466
  * https://docs.github.com/en/copilot/reference/custom-agents-configuration
66467
+ * https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference
66035
66468
  *
66036
66469
  * Per the configuration docs:
66037
66470
  * - `description` is required.
66038
66471
  * - `name`, `target`, `tools`, `model`, `disable-model-invocation`,
66039
66472
  * `user-invocable`, `mcp-servers`, and `metadata` are optional.
66473
+ * - The CLI command reference adds `models` (authored models in priority
66474
+ * order, overriding `model` when both are set) and `modelPolicy`
66475
+ * (`preferred` lets a `subagents` settings override or the `/subagents`
66476
+ * picker replace the model; `required` locks dispatch to the authored
66477
+ * list). CLI v1.0.81+; the shared reference page still lists `model` only.
66040
66478
  *
66041
66479
  * `looseObject` is used so that future additional fields are passed through
66042
66480
  * unchanged (consistent with the rest of rulesync's frontmatter schemas).
@@ -66047,6 +66485,8 @@ const CopilotCliSubagentFrontmatterSchema = z.looseObject({
66047
66485
  target: z.optional(z.string()),
66048
66486
  tools: z.optional(z.union([z.string(), z.array(z.string())])),
66049
66487
  model: z.optional(z.string()),
66488
+ models: z.optional(z.array(z.string())),
66489
+ modelPolicy: z.optional(z.enum(["preferred", "required"])),
66050
66490
  "disable-model-invocation": z.optional(z.boolean()),
66051
66491
  "user-invocable": z.optional(z.boolean()),
66052
66492
  "mcp-servers": z.optional(z.record(z.string(), z.unknown())),
@@ -70423,16 +70863,131 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
70423
70863
  }
70424
70864
  };
70425
70865
  //#endregion
70866
+ //#region src/utils/brace-aware-list.ts
70867
+ /**
70868
+ * Splits a comma-separated pattern list on the commas that separate its
70869
+ * entries, leaving the commas inside a brace group alone: in
70870
+ * `{src,lib}/**\/*.ts, tests/**\/*.test.ts` the first comma belongs to the
70871
+ * brace expansion and the second separates the two globs. Entries are trimmed;
70872
+ * empty entries are dropped.
70873
+ *
70874
+ * @example
70875
+ * splitBraceAwareList("src/**\/*.{ts,tsx}, docs/**")
70876
+ * // => ["src/**\/*.{ts,tsx}", "docs/**"]
70877
+ */
70878
+ const splitBraceAwareList = (value) => {
70879
+ const entries = [];
70880
+ let current = "";
70881
+ let braceDepth = 0;
70882
+ for (const char of value) {
70883
+ if (char === "{") braceDepth += 1;
70884
+ else if (char === "}" && braceDepth > 0) braceDepth -= 1;
70885
+ else if (char === "," && braceDepth === 0) {
70886
+ entries.push(current);
70887
+ current = "";
70888
+ continue;
70889
+ }
70890
+ current += char;
70891
+ }
70892
+ entries.push(current);
70893
+ return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
70894
+ };
70895
+ /**
70896
+ * Upper bound on the patterns one glob may expand to; past it the glob is
70897
+ * returned verbatim, since `{a,b}` repeated k times yields 2^k branches.
70898
+ */
70899
+ const MAX_BRACE_EXPANSIONS = 256;
70900
+ /** Matches an innermost brace group with at least one comma: `{a,b}`, not `{a}` or `{a,{b,c}}`. */
70901
+ const INNERMOST_BRACE_GROUP_REGEX = /\{([^{},]*(?:,[^{},]*)+)\}/;
70902
+ /**
70903
+ * Expands the comma alternations of a glob (`a.{ts,tsx}` → `a.ts`, `a.tsx`)
70904
+ * into one pattern per combination, innermost group first, so the result can
70905
+ * be handed to a consumer that splits its pattern list on bare commas. A glob
70906
+ * without a brace alternation is returned as is; a brace group without a
70907
+ * comma (e.g. `{a}`) is left untouched. A glob whose expansion would exceed
70908
+ * `MAX_BRACE_EXPANSIONS` patterns is returned as is as well.
70909
+ *
70910
+ * The cap is enforced on the work list itself rather than by counting the
70911
+ * groups up front: a nested group such as `{{a,b},c}` is rewritten into one
70912
+ * new group per inner alternative, so the number of patterns in flight is the
70913
+ * only bound that holds for arbitrary nesting.
70914
+ *
70915
+ * @example
70916
+ * expandBraceAlternations("src/**\/*.{ts,tsx}")
70917
+ * // => ["src/**\/*.ts", "src/**\/*.tsx"]
70918
+ */
70919
+ const expandBraceAlternations = (glob) => {
70920
+ let pending = [glob];
70921
+ for (;;) {
70922
+ const next = [];
70923
+ let expanded = false;
70924
+ for (const pattern of pending) {
70925
+ const match = INNERMOST_BRACE_GROUP_REGEX.exec(pattern);
70926
+ if (match === null) {
70927
+ next.push(pattern);
70928
+ continue;
70929
+ }
70930
+ expanded = true;
70931
+ const prefix = pattern.slice(0, match.index);
70932
+ const suffix = pattern.slice(match.index + match[0].length);
70933
+ for (const alternative of (match[1] ?? "").split(",")) next.push(`${prefix}${alternative}${suffix}`);
70934
+ if (next.length > MAX_BRACE_EXPANSIONS) return [glob];
70935
+ }
70936
+ if (!expanded) return [...new Set(next)];
70937
+ pending = next;
70938
+ }
70939
+ };
70940
+ //#endregion
70426
70941
  //#region src/features/rules/aiassistant-rule.ts
70427
70942
  /**
70943
+ * Rule types AI Assistant recognizes on the `apply:` metadata line. The values
70944
+ * are matched case-sensitively by the plugin, so they are emitted verbatim.
70945
+ * Any other value is carried through untouched (the plugin treats it as Off
70946
+ * today; a future build may recognize it).
70947
+ */
70948
+ const AIASSISTANT_APPLY_VALUES = [
70949
+ "always",
70950
+ "manually",
70951
+ "by model decision",
70952
+ "by file patterns",
70953
+ "off"
70954
+ ];
70955
+ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
70956
+ /**
70957
+ * Globs that match the whole project and so say nothing beyond "always".
70958
+ */
70959
+ const UNIVERSAL_GLOBS$2 = /* @__PURE__ */ new Set(["**/*", "*"]);
70960
+ const METADATA_DELIMITER = "---";
70961
+ /**
70962
+ * Splits text into lines that keep their own line terminator, so the body can
70963
+ * be sliced back out of the file byte-for-byte. Lone `\r` counts as a break,
70964
+ * matching Kotlin's `lines()`, which the plugin uses on the block.
70965
+ */
70966
+ const LINE_SPLIT_REGEX = /(?<=\r\n|\r(?!\n)|(?<!\r)\n)/;
70967
+ /**
70968
+ * The block is line-based, so a value must not carry a line break: one would
70969
+ * start a new `<field>: <value>` line and could override `apply:`.
70970
+ */
70971
+ const flattenValue = (value) => value.split(/\r\n|\r|\n/).map((part) => part.trim()).filter((part) => part.length > 0).join(" ");
70972
+ /**
70428
70973
  * Rule generator for JetBrains AI Assistant.
70429
70974
  *
70430
70975
  * AI Assistant reads project rules as flat Markdown files in
70431
- * `.aiassistant/rules/*.md` (one file per rule, identified by filename). The rule
70432
- * type (Always / Manually / By model decision / By file patterns) is configured
70433
- * in the IDE, not in the file, so the generated files are plain Markdown with no
70434
- * frontmatter. There is no special root file (unlike Junie's `guidelines.md`), so
70435
- * every rule — root or non-root — is written as its own file under `rules/`.
70976
+ * `.aiassistant/rules/*.md` (one file per rule, identified by filename). There
70977
+ * is no special root file (unlike Junie's `guidelines.md`), so every rule —
70978
+ * root or non-root — is written as its own file under `rules/`.
70979
+ *
70980
+ * The rule type (Always / Manually / By model decision / By file patterns /
70981
+ * Off) is stored in the file itself as a leading `---` metadata block that the
70982
+ * IDE's "Rule type" selector edits in place. The block is not YAML: each line
70983
+ * is `<field>: <value>` split on the first colon, and `patterns` is a
70984
+ * comma-separated glob list, so it is written and parsed by hand here. A file
70985
+ * without an `apply:` line is treated as Off at retrieval time (it is never
70986
+ * attached automatically), which is why every generated file carries one.
70987
+ *
70988
+ * Field names and values come from the plugin (`AiRulesService`,
70989
+ * `AiRulesLLMBundle.properties`; verified on build 262.10968.75) — the docs
70990
+ * page describes the rule type only as an IDE setting.
70436
70991
  *
70437
70992
  * AI Assistant and Junie are different JetBrains products (real-time assistance
70438
70993
  * vs. autonomous agent) with different layouts, so this is a separate target.
@@ -70440,18 +70995,154 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
70440
70995
  * @see https://www.jetbrains.com/help/ai-assistant/configure-project-rules.html
70441
70996
  */
70442
70997
  var AiassistantRule = class AiassistantRule extends ToolRule {
70998
+ body;
70999
+ metadata;
71000
+ constructor({ body, metadata, ...rest }) {
71001
+ super({
71002
+ ...rest,
71003
+ fileContent: AiassistantRule.buildFileContent({
71004
+ body,
71005
+ metadata
71006
+ })
71007
+ });
71008
+ this.body = body;
71009
+ this.metadata = metadata;
71010
+ }
70443
71011
  static getSettablePaths(_options = {}) {
70444
71012
  return { nonRoot: { relativeDirPath: AIASSISTANT_RULES_DIR_PATH } };
70445
71013
  }
71014
+ getBody() {
71015
+ return this.body;
71016
+ }
71017
+ getMetadata() {
71018
+ return this.metadata;
71019
+ }
71020
+ /**
71021
+ * Serialize the metadata block the way the IDE writes it
71022
+ * (`---\n<field>: <value>\n---`), followed by the body.
71023
+ */
71024
+ static buildFileContent({ body, metadata }) {
71025
+ if (metadata === void 0) return body;
71026
+ const lines = [`apply: ${metadata.apply}`];
71027
+ if (metadata.apply === "by model decision" && metadata.instructions) lines.push(`instructions: ${flattenValue(metadata.instructions)}`);
71028
+ if (metadata.apply === "by file patterns" && metadata.patterns && metadata.patterns.length > 0) lines.push(`patterns: ${metadata.patterns.map(flattenValue).join(", ")}`);
71029
+ return `${METADATA_DELIMITER}\n${lines.join("\n")}\n${METADATA_DELIMITER}\n\n${body}`;
71030
+ }
71031
+ /**
71032
+ * Split a rule file into its metadata block and body. The plugin locates the
71033
+ * block with `^---\s*([\s\S]*?)---\s*` (anchored at the very start of the
71034
+ * file); this reads it line by line instead — a delimiter is a line that is
71035
+ * exactly `---` — which is linear on any input and tolerates a BOM or blank
71036
+ * lines before the block. Each line is split on its first colon and trimmed;
71037
+ * unknown fields are ignored; a missing block or `apply:` line yields no
71038
+ * metadata, while an unrecognized `apply:` value is kept verbatim.
71039
+ */
71040
+ static parseFileContent(fileContent) {
71041
+ const content = fileContent.replace(/^\uFEFF/, "").trimStart();
71042
+ const lines = content.split(LINE_SPLIT_REGEX);
71043
+ const closingIndex = lines[0]?.trim() === METADATA_DELIMITER ? lines.findIndex((line, index) => index > 0 && line.trim() === METADATA_DELIMITER) : -1;
71044
+ if (closingIndex === -1) return {
71045
+ metadata: void 0,
71046
+ body: content.trim()
71047
+ };
71048
+ const body = lines.slice(closingIndex + 1).join("").trim();
71049
+ const fields = /* @__PURE__ */ new Map();
71050
+ for (const line of lines.slice(1, closingIndex)) {
71051
+ const separatorIndex = line.indexOf(":");
71052
+ if (separatorIndex === -1) continue;
71053
+ fields.set(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
71054
+ }
71055
+ const apply = fields.get("apply");
71056
+ if (apply === void 0 || apply.length === 0) return {
71057
+ metadata: void 0,
71058
+ body
71059
+ };
71060
+ const instructions = fields.get("instructions");
71061
+ const patterns = fields.get("patterns");
71062
+ return {
71063
+ metadata: {
71064
+ apply,
71065
+ ...apply === "by model decision" && instructions && { instructions },
71066
+ ...apply === "by file patterns" && patterns && { patterns: splitBraceAwareList(patterns) }
71067
+ },
71068
+ body
71069
+ };
71070
+ }
71071
+ /**
71072
+ * Derive the metadata block from the canonical frontmatter, mirroring the
71073
+ * Cursor mapping: an explicit `aiassistant.apply` wins; otherwise specific
71074
+ * `globs` become `by file patterns`, a `description` on a rule with no
71075
+ * `globs` at all becomes the `by model decision` condition, and everything
71076
+ * else — the root rule, universal globs (which already say "every file",
71077
+ * whatever the description), bare rules — is `always`.
71078
+ *
71079
+ * The plugin splits `patterns` on every comma, so a brace alternation such
71080
+ * as `*.{ts,tsx}` is expanded into one glob per branch before it is written.
71081
+ */
71082
+ static buildMetadata(frontmatter) {
71083
+ const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
71084
+ const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$2.has(glob));
71085
+ const instructions = frontmatter.description && flattenValue(frontmatter.description);
71086
+ const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
71087
+ const apply = explicitApply ? explicitApply : frontmatter.root !== true && globs.length > 0 && !hasUniversalGlob ? "by file patterns" : frontmatter.root !== true && globs.length === 0 && instructions ? "by model decision" : "always";
71088
+ const patterns = globs;
71089
+ return {
71090
+ apply,
71091
+ ...apply === "by model decision" && instructions && { instructions },
71092
+ ...apply === "by file patterns" && patterns.length > 0 && { patterns }
71093
+ };
71094
+ }
71095
+ /**
71096
+ * Warn, at generate time, about a block the plugin cannot act on as the
71097
+ * author presumably intended.
71098
+ */
71099
+ static warnAboutMetadata({ metadata, relativeFilePath }) {
71100
+ const { apply, instructions, patterns = [] } = metadata;
71101
+ if (!isAiassistantApply(apply)) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply "${apply}" is not one of ${AIASSISTANT_APPLY_VALUES.join(", ")}; it is written as is, and the current AI Assistant treats such a rule as Off.`);
71102
+ if (apply === "by file patterns" && patterns.length === 0) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by file patterns" but the rule has no globs, so AI Assistant will never attach it.`);
71103
+ if (apply === "by model decision" && !instructions) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by model decision" but the rule has no description, so AI Assistant has no condition to decide on.`);
71104
+ const commaGlob = patterns.find((glob) => glob.includes(","));
71105
+ if (commaGlob !== void 0) warnWithFallback(void 0, `${relativeFilePath}: the glob "${commaGlob}" contains a comma, which AI Assistant reads as a pattern separator.`);
71106
+ if ([
71107
+ apply,
71108
+ instructions ?? "",
71109
+ ...patterns
71110
+ ].some((value) => value.includes("---"))) warnWithFallback(void 0, `${relativeFilePath}: a metadata value contains "---", which AI Assistant reads as the end of the metadata block.`);
71111
+ }
70446
71112
  toRulesyncRule() {
70447
- return this.toRulesyncRuleDefault();
71113
+ const metadata = this.metadata;
71114
+ const globs = metadata?.apply === "by file patterns" ? metadata.patterns ?? [] : metadata?.apply === "always" ? ["**/*"] : [];
71115
+ const frontmatter = {
71116
+ root: false,
71117
+ targets: ["*"],
71118
+ description: metadata?.apply === "by model decision" ? metadata.instructions : void 0,
71119
+ globs
71120
+ };
71121
+ const derivedApply = metadata === void 0 ? void 0 : AiassistantRule.buildMetadata(frontmatter).apply;
71122
+ const apply = metadata !== void 0 && derivedApply !== metadata.apply ? metadata.apply : void 0;
71123
+ return new RulesyncRule({
71124
+ outputRoot: process.cwd(),
71125
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
71126
+ relativeFilePath: this.getRelativeFilePath(),
71127
+ frontmatter: {
71128
+ ...frontmatter,
71129
+ ...apply !== void 0 && { aiassistant: { apply } }
71130
+ },
71131
+ body: this.body
71132
+ });
70448
71133
  }
70449
71134
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
71135
+ const metadata = this.buildMetadata(rulesyncRule.getFrontmatter());
71136
+ this.warnAboutMetadata({
71137
+ metadata,
71138
+ relativeFilePath: rulesyncRule.getRelativeFilePath()
71139
+ });
70450
71140
  return new AiassistantRule({
70451
71141
  outputRoot,
70452
71142
  relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
70453
71143
  relativeFilePath: rulesyncRule.getRelativeFilePath(),
70454
- fileContent: rulesyncRule.getBody(),
71144
+ body: rulesyncRule.getBody(),
71145
+ metadata,
70455
71146
  validate,
70456
71147
  root: false
70457
71148
  });
@@ -70459,11 +71150,13 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
70459
71150
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
70460
71151
  const relativeDirPath = this.getSettablePaths().nonRoot.relativeDirPath;
70461
71152
  const fileContent = await readFileContent(join(outputRoot, relativeDirPath, relativeFilePath));
71153
+ const { metadata, body } = this.parseFileContent(fileContent);
70462
71154
  return new AiassistantRule({
70463
71155
  outputRoot,
70464
71156
  relativeDirPath,
70465
71157
  relativeFilePath,
70466
- fileContent: fileContent.trim(),
71158
+ body,
71159
+ metadata,
70467
71160
  validate,
70468
71161
  root: false
70469
71162
  });
@@ -70473,7 +71166,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
70473
71166
  outputRoot,
70474
71167
  relativeDirPath,
70475
71168
  relativeFilePath,
70476
- fileContent: "",
71169
+ body: "",
70477
71170
  validate: false,
70478
71171
  root: false
70479
71172
  });
@@ -72030,40 +72723,14 @@ const CodebuddyRuleFrontmatterSchema = z.object({
72030
72723
  enabled: z.optional(z.boolean())
72031
72724
  });
72032
72725
  /**
72033
- * Splits a scalar `paths` value on the commas that separate patterns, leaving
72034
- * the commas inside a brace group alone: CodeBuddy's memory docs say "You can
72035
- * also combine multiple patterns with commas" and give
72036
- * `paths: {src,lib}/**\/*.ts, tests/**\/*.test.ts` as the example, where the
72037
- * first comma belongs to the brace expansion and the second separates the two
72038
- * globs. Only a scalar is split — the docs describe the comma form for the
72039
- * string shape, and a list already separates its patterns.
72040
- */
72041
- function splitCodebuddyPathsScalar(paths) {
72042
- const patterns = [];
72043
- let current = "";
72044
- let braceDepth = 0;
72045
- for (const char of paths) {
72046
- if (char === "{") braceDepth += 1;
72047
- else if (char === "}" && braceDepth > 0) braceDepth -= 1;
72048
- else if (char === "," && braceDepth === 0) {
72049
- patterns.push(current);
72050
- current = "";
72051
- continue;
72052
- }
72053
- current += char;
72054
- }
72055
- patterns.push(current);
72056
- return patterns.map((pattern) => pattern.trim());
72057
- }
72058
- /**
72059
72726
  * Normalizes the documented `string` / `string[]` shapes of `paths` to the
72060
72727
  * list form the rest of the adapter works with, splitting a comma-separated
72061
- * scalar into its patterns (see `splitCodebuddyPathsScalar`). An empty list
72728
+ * scalar into its patterns (see `splitBraceAwareList`). An empty list
72062
72729
  * and an empty string both mean "no paths".
72063
72730
  */
72064
72731
  function normalizeCodebuddyPaths(paths) {
72065
72732
  if (paths === void 0) return;
72066
- const list = (typeof paths === "string" ? splitCodebuddyPathsScalar(paths) : paths).filter((path) => path.trim() !== "");
72733
+ const list = (typeof paths === "string" ? splitBraceAwareList(paths) : paths).filter((path) => path.trim() !== "");
72067
72734
  return list.length > 0 ? list : void 0;
72068
72735
  }
72069
72736
  /**
@@ -80280,4 +80947,4 @@ async function importChecksCore(params) {
80280
80947
  //#endregion
80281
80948
  export { RulesyncCommand as $, toPosixPath as $t, CLAUDECODE_MEMORIES_DIR_NAME as A, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as An, checkPathTraversal as At, RulesyncSubagent as B, stripControlCharacters as Bn, listDirectoryEntryNames as Bt, COMMANDCODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, withWarnOnceScope as Ct, CODEXCLI_DIR as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, assertDirectoryIfExists as Dt, CODEXCLI_BASH_RULES_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, applyFileMode as Et, caseFoldIdentity as F, formatError as Fn, getFileSize as Ft, RulesyncRuleFrontmatterSchema as G, readFileContentOrNull as Gt, RulesyncSkill as H, stripHiddenCharacters as Hn, listSubdirectoryNames as Ht, groupSpellingsByCaseFoldedIdentity as I, truncateText as In, getHomeDirectory as It, RulesyncIgnore as J, removeFile as Jt, RulesyncPermissions as K, removeDirectory as Kt, AUGMENTCODE_DIR as L, hasDeceptiveHiddenCharacters as Ln, isFileNotFoundError as Lt, CLAUDECODE_SKILLS_DIR_PATH as M, ALL_FEATURES as Mn, directoryExists as Mt, FACTORYDROID_DIR as N, ALL_FEATURES_WITH_WILDCARD as Nn, ensureDir as Nt, CLAUDECODE_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, assertTreeContainsNoSymlinks as Ot, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, fileExists as Pt, parseJsonc as Q, runWithDirectoryRollback as Qt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as R, hasEnclosingMarkOutsideKeycap as Rn, isFileSystemError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, resetRunWarningState as St, ChecksProcessor as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, ErrorCodes as Tt, RulesyncSkillFrontmatterSchema as U, pathEscapesRoot as Ut, RulesyncSubagentFrontmatterSchema as V, stripControlCharactersKeepingLineFeeds as Vn, listFilePathsRecursively as Vt, RulesyncRule as W, readFileContent as Wt, getRulesyncSourceCandidates as X, removeTempDirectory as Xt, RulesyncHooks as Y, removeFileStrict as Yt, resolveRulesyncSourceWritePath as Z, resolvePath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, JsonLogger as _t, getProcessorRegistryEntry as a, ToolTargetSchema as an, SHARED_USER_MANAGED_CONFIG_PATHS as at, CommandsProcessor as b, RULESYNC_MCP_SCHEMA_URL as bn, warnOnConflictingFlags as bt, RulesProcessor as c, MAX_FILE_SIZE as cn, mergeInputRootConfigs as ct, CODEBUDDY_DIR as d, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as dn, ConfigFileSchema as dt, writeFileBuffer as en, RulesyncCommandFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CONFIG_RELATIVE_FILE_PATH as fn, GITIGNORE_DESTINATION_KEY as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, ConsoleLogger as gt, shortenToWidth as h, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as hn, findControlCharacter as ht, inspectInputRoots as i, PACKAGING_TOOL_TARGETS as in, loadYaml as it, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, createTempDirectory as jt, CLAUDECODE_LOCAL_RULE_FILE_NAME as k, RULESYNC_USER_CONFIG_DIR_NAME as kn, assertWritablePathInsideRoot as kt, SubagentsProcessor as l, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ln, resolveEffectiveInputRoots as lt, displayWidthOf as m, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as mn, assertTargetsFeaturesExclusive as mt, formatSourceLoadFailure as n, ALL_TOOL_TARGETS as nn, RulesyncCheckFrontmatterSchema as nt, convertFromTool as o, CURATED_RULES_FEATURE_SUBDIR as on, SKILL_FILE_NAME as ot, ELLIPSIS_WIDTH as p, RULESYNC_CONFIG_SCHEMA_URL as pn, SourceEntrySchema as pt, RulesyncMcp as q, removeDirectoryStrict as qt, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rn, stringifyFrontmatter as rt, isPackagingToolTarget as s, FEATURE_SOURCE_TREE_ENTRIES as sn, ConfigResolver as st, importFromTool as t, writeFileContent as tn, RulesyncCheck as tt, SkillsProcessor as u, RULESYNC_CHECKS_RELATIVE_DIR_PATH as un, CONFLICTING_TARGET_PAIRS as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, WarningCollectingLogger as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, CLIError as wt, QWENCODE_DIR as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, withFallbackLoggerTarget as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, fallbackLogger as yt, getLocalSkillDirNames as z, quoteForLog as zn, isSymlink as zt };
80282
80949
 
80283
- //# sourceMappingURL=import-D_iukHSy.js.map
80950
+ //# sourceMappingURL=import-BiyWNVZY.js.map