rulesync 16.37.0 → 16.39.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.
@@ -1,8 +1,8 @@
1
1
  import { ZodError } from "zod";
2
2
  import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
3
3
  import { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat, writeFile } from "node:fs/promises";
4
- import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
5
- import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse, parseTree, printParseErrorCode } from "jsonc-parser";
4
+ import path, { basename, dirname, extname, isAbsolute, join, normalize, parse, posix, relative, resolve, sep, win32 } from "node:path";
5
+ import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse as parse$1, parseTree, printParseErrorCode } from "jsonc-parser";
6
6
  import { constants } from "node:fs";
7
7
  import os from "node:os";
8
8
  import { intersection, kebabCase, uniq } from "es-toolkit";
@@ -14,7 +14,7 @@ import { YAMLException, dump, load } from "js-yaml";
14
14
  import { omit } from "es-toolkit/object";
15
15
  import { createHash } from "node:crypto";
16
16
  import * as smolToml from "smol-toml";
17
- import { parse as parse$1, stringify } from "smol-toml";
17
+ import { parse as parse$2, stringify } from "smol-toml";
18
18
  import { encode } from "@toon-format/toon";
19
19
  //#region src/utils/control-characters.ts
20
20
  /**
@@ -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",
@@ -670,6 +671,7 @@ const subagentsProcessorToolTargetTuple = [
670
671
  "kiro-cli",
671
672
  "kiro-ide",
672
673
  "opencode",
674
+ "pool",
673
675
  "qwencode",
674
676
  "reasonix",
675
677
  "roo",
@@ -751,6 +753,7 @@ const hooksProcessorToolTargetTuple = [
751
753
  "crush",
752
754
  "opencode",
753
755
  "pi",
756
+ "pool",
754
757
  "factorydroid",
755
758
  "goose",
756
759
  "hermesagent",
@@ -798,6 +801,7 @@ const permissionsProcessorToolTargetTuple = [
798
801
  "kiro-ide",
799
802
  "opencode",
800
803
  "pi",
804
+ "pool",
801
805
  "qwencode",
802
806
  "reasonix",
803
807
  "roo",
@@ -1354,6 +1358,111 @@ async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
1354
1358
  }));
1355
1359
  }
1356
1360
  /**
1361
+ * How many links {@link writeLandingPath} follows before it treats the chain as
1362
+ * a cycle it cannot write through. Linux itself gives up after 40.
1363
+ */
1364
+ const MAX_LINK_HOPS = 40;
1365
+ /**
1366
+ * Whether `error` is the ENOTDIR a lookup through a file raises. On the way to
1367
+ * a write it means the same as a missing entry: nothing at or below the path
1368
+ * exists, so nothing there can be a link.
1369
+ */
1370
+ function isNotADirectoryError(error) {
1371
+ return someErrorInChain(error, (candidate) => "code" in candidate && candidate.code === "ENOTDIR");
1372
+ }
1373
+ /**
1374
+ * Where a write to `targetPath` lands once the OS has followed every link on
1375
+ * the way there, or `null` when the links form a cycle no write can get
1376
+ * through. The target itself usually does not exist yet, so the walk goes
1377
+ * segment by segment from the filesystem root: an existing segment that is a
1378
+ * link is replaced by what it resolves to, and a dangling link — one whose
1379
+ * target does not exist yet, which is exactly the shape of a link planted at
1380
+ * `AGENTS.md` pointing at a `~/.zshenv` that is still to be created — has its
1381
+ * target spliced into the walk in its place, so the segments of that target
1382
+ * are looked up one by one too. Once a segment is missing, nothing below it
1383
+ * exists, and the rest of the path is where the write's `mkdir -p` will put it.
1384
+ *
1385
+ * Walking segment by segment is what keeps a `..` honest. A link target is a
1386
+ * path the OS resolves left to right, so in `sub/../victim` the `..` climbs out
1387
+ * of wherever `sub` leads, not out of the directory `sub` is spelled in; a
1388
+ * lexical `resolve` folds `sub/..` away before `sub` is ever looked at, which
1389
+ * is how a link chain could be spelled to pass the check and still land
1390
+ * outside the root.
1391
+ */
1392
+ async function writeLandingPath(targetPath) {
1393
+ const start = splitAbsolutePath(isAbsolute(targetPath) ? targetPath : resolve(targetPath));
1394
+ let current = start.root;
1395
+ let pending = start.segments;
1396
+ let exists = true;
1397
+ let linkHops = 0;
1398
+ while (pending.length > 0) {
1399
+ const segment = pending.shift();
1400
+ if (segment === void 0 || segment === "" || segment === ".") continue;
1401
+ if (segment === "..") {
1402
+ current = dirname(current);
1403
+ continue;
1404
+ }
1405
+ current = join(current, segment);
1406
+ if (!exists) continue;
1407
+ let stats;
1408
+ try {
1409
+ stats = await lstat(current);
1410
+ } catch (error) {
1411
+ if (!isFileNotFoundError(error) && !isNotADirectoryError(error)) throw error;
1412
+ exists = false;
1413
+ continue;
1414
+ }
1415
+ if (!stats.isSymbolicLink()) continue;
1416
+ linkHops += 1;
1417
+ if (linkHops > MAX_LINK_HOPS) return null;
1418
+ try {
1419
+ current = await realpath(current);
1420
+ continue;
1421
+ } catch {}
1422
+ const linkTarget = await readlink(current);
1423
+ if (isAbsolute(linkTarget)) {
1424
+ const target = splitAbsolutePath(linkTarget);
1425
+ current = target.root;
1426
+ pending = [...target.segments, ...pending];
1427
+ } else {
1428
+ current = dirname(current);
1429
+ pending = [...splitPathSegments(linkTarget), ...pending];
1430
+ }
1431
+ }
1432
+ return current;
1433
+ }
1434
+ /**
1435
+ * Split an absolute path into its root and its raw segments, without folding a
1436
+ * `..` away: {@link writeLandingPath} has to see every `..` where it is spelled.
1437
+ */
1438
+ function splitAbsolutePath(absolutePath) {
1439
+ const root = parse(absolutePath).root;
1440
+ return {
1441
+ root,
1442
+ segments: splitPathSegments(absolutePath.slice(root.length))
1443
+ };
1444
+ }
1445
+ /**
1446
+ * Whether writing `targetPath` would land outside `rootPath` once every link on
1447
+ * the way there is resolved. Both sides are taken to where a write to them
1448
+ * would land (see {@link writeLandingPath}), so a root that does not fully
1449
+ * exist yet is judged the same way as the target below it.
1450
+ *
1451
+ * A link that stays inside the root passes. That is what sets this apart from
1452
+ * the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
1453
+ * dotfiles checkout linked from inside the home directory is a common shape for
1454
+ * a global write target, and refusing it would refuse `--global` outright.
1455
+ *
1456
+ * A link chain that cycles is reported as an escape: the write could not get
1457
+ * through it, and refusing it is the answer that never writes somewhere
1458
+ * unexpected.
1459
+ */
1460
+ async function writablePathEscapesRoot({ rootPath, targetPath }) {
1461
+ const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
1462
+ if (rootLanding === null || targetLanding === null) return true;
1463
+ return pathEscapesRoot(relative(rootLanding, targetLanding));
1464
+ }
1465
+ /**
1357
1466
  * How many trailing segments `filePath` and `identity` have in common.
1358
1467
  *
1359
1468
  * A path that walked through no link at all shares all of its own segments with
@@ -2935,7 +3044,7 @@ const getDefaults = () => ({
2935
3044
  const loadConfigFromFile = async (filePath) => {
2936
3045
  if (!await fileExists(filePath)) return {};
2937
3046
  const fileContent = await readFileContent(filePath);
2938
- const jsonData = parse(fileContent);
3047
+ const jsonData = parse$1(fileContent);
2939
3048
  const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
2940
3049
  assertTargetsFeaturesExclusive({
2941
3050
  targets: configParams.targets,
@@ -3304,6 +3413,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3304
3413
  ".grok/config.toml",
3305
3414
  ".rovodev/config.yml",
3306
3415
  ".rovodev/mcp.json",
3416
+ ".poolside/settings.yaml",
3417
+ ".config/poolside/settings.yaml",
3307
3418
  ".tabnine/agent/settings.json",
3308
3419
  ".vibe/config.toml",
3309
3420
  ".zcode/config.json",
@@ -4353,7 +4464,7 @@ const PI_HOOK_EVENTS = [
4353
4464
  * adapter. Amp's `agent.start` / `agent.end` events describe the main agent
4354
4465
  * turn, not a subagent lifecycle, so they map to `beforeSubmitPrompt` / `stop`.
4355
4466
  *
4356
- * @see https://ampcode.com/manual/plugin-api
4467
+ * @see https://ampcode.com/docs/plugin-api
4357
4468
  */
4358
4469
  const AMP_HOOK_EVENTS = [
4359
4470
  "sessionStart",
@@ -4651,6 +4762,34 @@ const VIBE_HOOK_EVENTS = [
4651
4762
  "stop"
4652
4763
  ];
4653
4764
  /**
4765
+ * Hook events supported by Pool.
4766
+ *
4767
+ * Pool's `hooks` settings block fires six events, spelled the Claude Code
4768
+ * way: `PreToolUse`, `PostToolUse`, `UserPromptSubmit` (← `beforeSubmitPrompt`),
4769
+ * `PreCompact`, `SessionStart` and `Stop`. Every entry carries a `matcher`
4770
+ * (required by Pool's schema; `""`/`*` match any tool, a bare name is an exact
4771
+ * match, `a|b` lists names, anything else is a regular expression), which only
4772
+ * `PreToolUse`/`PostToolUse` test against the tool name.
4773
+ * @see https://docs.poolside.ai/hooks
4774
+ */
4775
+ const POOL_HOOK_EVENTS = [
4776
+ "preToolUse",
4777
+ "postToolUse",
4778
+ "beforeSubmitPrompt",
4779
+ "preCompact",
4780
+ "sessionStart",
4781
+ "stop"
4782
+ ];
4783
+ const CANONICAL_TO_POOL_EVENT_NAMES = {
4784
+ preToolUse: "PreToolUse",
4785
+ postToolUse: "PostToolUse",
4786
+ beforeSubmitPrompt: "UserPromptSubmit",
4787
+ preCompact: "PreCompact",
4788
+ sessionStart: "SessionStart",
4789
+ stop: "Stop"
4790
+ };
4791
+ const POOL_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_EVENT_NAMES).map(([k, v]) => [v, k]));
4792
+ /**
4654
4793
  * Hook events supported by Crush.
4655
4794
  *
4656
4795
  * Crush's `hooks` config block currently fires a single event, `PreToolUse`
@@ -5245,6 +5384,7 @@ const HooksConfigSchema = z.looseObject({
5245
5384
  "kimi-code": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5246
5385
  zcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5247
5386
  crush: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5387
+ pool: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5248
5388
  bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5249
5389
  cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5250
5390
  commandcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
@@ -5919,7 +6059,7 @@ function parseJsonc(content) {
5919
6059
  */
5920
6060
  function parseStrict(content) {
5921
6061
  const errors = [];
5922
- const result = parse(content, errors, {
6062
+ const result = parse$1(content, errors, {
5923
6063
  allowTrailingComma: true,
5924
6064
  disallowComments: false
5925
6065
  });
@@ -6198,6 +6338,7 @@ const McpServerSchema = z.looseObject({
6198
6338
  crushOauthClientSecret: z.optional(z.string()),
6199
6339
  crushOauthCallbackPort: z.optional(z.int()),
6200
6340
  crushSessionless: z.optional(z.boolean()),
6341
+ poolAllow: z.optional(z.array(z.string())),
6201
6342
  headers: z.optional(z.record(z.string(), z.string())),
6202
6343
  /**
6203
6344
  * The canonical per-server tool allowlist.
@@ -6629,6 +6770,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
6629
6770
  "crushOauthClientSecret",
6630
6771
  "crushOauthCallbackPort",
6631
6772
  "crushSessionless",
6773
+ "poolAllow",
6632
6774
  "oauth_client_id",
6633
6775
  "oauth_client_secret",
6634
6776
  "oauth_callback_port",
@@ -7971,6 +8113,7 @@ const PermissionsConfigSchema = z.looseObject({
7971
8113
  copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
7972
8114
  crush: z.optional(CanonicalPermissionsOverrideSchema),
7973
8115
  goose: z.optional(CanonicalPermissionsOverrideSchema),
8116
+ pool: z.optional(CanonicalPermissionsOverrideSchema),
7974
8117
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
7975
8118
  "kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
7976
8119
  roo: z.optional(CanonicalPermissionsOverrideSchema),
@@ -8316,7 +8459,7 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
8316
8459
  * them; treating a segment that carries one as static could name a directory
8317
8460
  * that no matched file actually lives under.
8318
8461
  */
8319
- const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
8462
+ const GLOB_METACHARACTERS$1 = /[*?[\]{}()!]/;
8320
8463
  /**
8321
8464
  * The directory every file matched by `glob` lives under, or `undefined` when
8322
8465
  * the pattern pins down no such directory.
@@ -8349,7 +8492,7 @@ function getGlobStaticPrefix(glob) {
8349
8492
  const lastIndex = segments.length - 1;
8350
8493
  const prefix = [];
8351
8494
  for (const [index, segment] of segments.entries()) {
8352
- if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
8495
+ if (segment === "" || GLOB_METACHARACTERS$1.test(segment)) break;
8353
8496
  if (segment === ".") continue;
8354
8497
  if (index === lastIndex) break;
8355
8498
  prefix.push(segment);
@@ -9813,6 +9956,22 @@ allowedMcpServers: z.optional(z.array(z.string())) })),
9813
9956
  injectAgentsMd: z.optional(z.boolean()),
9814
9957
  mcpServers: z.optional(z.array(z.string()))
9815
9958
  })),
9959
+ pool: z.optional(z.looseObject({
9960
+ type: z.optional(z.enum([
9961
+ "in_process",
9962
+ "command",
9963
+ "agent_server"
9964
+ ])),
9965
+ description: z.optional(z.string()),
9966
+ instructions: z.optional(z.string()),
9967
+ command: z.optional(z.string()),
9968
+ args: z.optional(z.array(z.string())),
9969
+ env: z.optional(z.record(z.string(), z.string())),
9970
+ agent_server: z.optional(z.string()),
9971
+ session_config_options: z.optional(z.record(z.string(), z.unknown())),
9972
+ inherit_agent_config: z.optional(z.boolean()),
9973
+ disabled: z.optional(z.boolean())
9974
+ })),
9816
9975
  vibe: z.optional(z.looseObject({
9817
9976
  agent_type: z.optional(z.enum(["agent", "subagent"])),
9818
9977
  display_name: z.optional(z.string()),
@@ -10174,7 +10333,7 @@ function tryParseStructured({ filePath, content }) {
10174
10333
  case ".json":
10175
10334
  case ".jsonc": {
10176
10335
  const errors = [];
10177
- const value = parse(content, errors);
10336
+ const value = parse$1(content, errors);
10178
10337
  return errors.length > 0 ? PARSE_FAILED : {
10179
10338
  ok: true,
10180
10339
  value
@@ -10400,6 +10559,17 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
10400
10559
  this.logger = logger;
10401
10560
  }
10402
10561
  /**
10562
+ * Whether `convertRulesyncFilesToToolFiles` still yields tool files for an
10563
+ * empty source list. A processor that aggregates into a shared file it may
10564
+ * never delete (Pool's `settings.yaml` subagents) answers true so a run
10565
+ * whose last source file is gone retracts what an earlier run generated;
10566
+ * the default keeps a run with no source from writing anything. Consulted
10567
+ * after `loadRulesyncFiles`, so the answer may depend on what that load saw.
10568
+ */
10569
+ emitsToolFilesForEmptySource() {
10570
+ return false;
10571
+ }
10572
+ /**
10403
10573
  * Return tool targets that this feature supports.
10404
10574
  */
10405
10575
  static getToolTargets(_params = {}) {
@@ -10414,6 +10584,11 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
10414
10584
  const changedPaths = [];
10415
10585
  for (const aiFile of aiFiles) {
10416
10586
  const filePath = aiFile.getFilePath();
10587
+ if (await refusesWriteOutsideRoot({
10588
+ logger: this.logger,
10589
+ rootPath: aiFile.getOutputRoot(),
10590
+ targetPath: filePath
10591
+ })) continue;
10417
10592
  const existingFileContent = await readFileContentOrNull(filePath);
10418
10593
  if (existingFileContent !== null && aiFile.shouldMergeExistingFileContent()) aiFile.setFileContent(existingFileContent);
10419
10594
  const contentWithNewline = addTrailingNewline(aiFile.getFileContent());
@@ -10546,6 +10721,37 @@ function mergeByIdentity({ perRoot, identity }) {
10546
10721
  return order.map((key) => winnerByKey.get(key));
10547
10722
  }
10548
10723
  /**
10724
+ * Whether a write to `targetPath` has to be refused because a link on the way
10725
+ * there leads out of `rootPath`, warning about the refusal.
10726
+ *
10727
+ * A checked-out repository can carry a symbolic link at an output path
10728
+ * (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`), and a write
10729
+ * through it would land wherever the link points. Every writer that lets the
10730
+ * OS follow links — file, directory, and config-rewrite alike — asks this first
10731
+ * so that one warning describes the one behavior.
10732
+ */
10733
+ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
10734
+ if (!await writablePathEscapesRoot({
10735
+ rootPath,
10736
+ targetPath
10737
+ })) return false;
10738
+ logger.warn(`Refusing to write ${quoteForLog(targetPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
10739
+ return true;
10740
+ }
10741
+ /**
10742
+ * {@link refusesWriteOutsideRoot} over every path a single unit of output
10743
+ * writes, stopping at the first refusal: a directory written as one unit is
10744
+ * held back whole when any one file in it would land outside the root.
10745
+ */
10746
+ async function refusesAnyWriteOutsideRoot({ logger, rootPath, targetPaths }) {
10747
+ for (const targetPath of targetPaths) if (await refusesWriteOutsideRoot({
10748
+ logger,
10749
+ rootPath,
10750
+ targetPath
10751
+ })) return true;
10752
+ return false;
10753
+ }
10754
+ /**
10549
10755
  * The key two spellings share when a case-insensitive filesystem would give
10550
10756
  * them one file. `toLowerCase()` is locale-independent (unlike
10551
10757
  * `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
@@ -12219,16 +12425,16 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
12219
12425
  let parsed;
12220
12426
  try {
12221
12427
  if (format === "yaml") parsed = loadYaml(fileContent);
12222
- else if (format === "toml") parsed = parse$1(fileContent);
12428
+ else if (format === "toml") parsed = parse$2(fileContent);
12223
12429
  else if (format === "json") parsed = JSON.parse(fileContent);
12224
12430
  else if (jsoncParseErrors === "error") {
12225
12431
  const errors = [];
12226
- parsed = parse(fileContent, errors, { allowTrailingComma: true });
12432
+ parsed = parse$1(fileContent, errors, { allowTrailingComma: true });
12227
12433
  if (errors.length > 0) {
12228
12434
  const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
12229
12435
  throw new Error(details);
12230
12436
  }
12231
- } else parsed = parse(fileContent);
12437
+ } else parsed = parse$1(fileContent);
12232
12438
  } catch (error) {
12233
12439
  throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
12234
12440
  }
@@ -12924,6 +13130,8 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
12924
13130
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
12925
13131
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
12926
13132
  const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
13133
+ const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
13134
+ const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
12927
13135
  /**
12928
13136
  * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
12929
13137
  * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
@@ -13005,6 +13213,38 @@ const KIMI_CODE_CONFIG_DECLARATION = {
13005
13213
  }
13006
13214
  };
13007
13215
  /**
13216
+ * Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
13217
+ * the user's primary Pool settings file, so an unparseable root fails closed
13218
+ * rather than being replaced with generated output. `mcp_servers` is owned as
13219
+ * a whole key because the MCP writer emits the complete server map. The
13220
+ * permissions writer deep-merges: it rewrites the `allow`/`deny` lists of the
13221
+ * `tools.<name>` blocks it manages and the `paths` lists, leaving each tool's
13222
+ * `disabled` flag, unmanaged tools and every other key in place. `hooks` is
13223
+ * owned as a whole key because the hooks writer emits every event list.
13224
+ * `subagents` is owned as a whole key too: its writer recomputes the block
13225
+ * from the existing file (`default` and the built-in `general` agent carried
13226
+ * over, every other agent replaced) before applying the patch.
13227
+ */
13228
+ const POOL_SETTINGS_DECLARATION = {
13229
+ format: "yaml",
13230
+ invalidRootPolicy: "error",
13231
+ features: {
13232
+ mcp: {
13233
+ kind: "replace-owned-keys",
13234
+ ownedKeys: ["mcp_servers"]
13235
+ },
13236
+ permissions: { kind: "deep-merge" },
13237
+ hooks: {
13238
+ kind: "replace-owned-keys",
13239
+ ownedKeys: ["hooks"]
13240
+ },
13241
+ subagents: {
13242
+ kind: "replace-owned-keys",
13243
+ ownedKeys: ["subagents"]
13244
+ }
13245
+ }
13246
+ };
13247
+ /**
13008
13248
  * ZCode's settings file, which also carries model/theme/permission keys
13009
13249
  * rulesync does not own. Both copies are the user's primary ZCode config, so
13010
13250
  * every writer refuses to read-modify-write a file it could not parse rather
@@ -13109,6 +13349,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13109
13349
  [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13110
13350
  [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13111
13351
  [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13352
+ [POOL_PROJECT_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
13353
+ [POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
13112
13354
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
13113
13355
  format: "yaml",
13114
13356
  invalidRootPolicy: "error",
@@ -19278,7 +19520,7 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
19278
19520
  }
19279
19521
  parseTomlContent(content) {
19280
19522
  try {
19281
- const parsed = parse$1(content);
19523
+ const parsed = parse$2(content);
19282
19524
  const result = TabnineCommandFrontmatterSchema.safeParse(parsed);
19283
19525
  if (!result.success) throw new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`);
19284
19526
  return {
@@ -20395,6 +20637,11 @@ var CommandsProcessor = class extends FeatureProcessor {
20395
20637
  global: this.global,
20396
20638
  relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
20397
20639
  }));
20640
+ if (await refusesWriteOutsideRoot({
20641
+ logger: this.logger,
20642
+ rootPath: this.outputRoot,
20643
+ targetPath: configPath
20644
+ })) return changedCount;
20398
20645
  const currentContent = await readFileContentOrNull(configPath);
20399
20646
  if (currentContent === null) return changedCount;
20400
20647
  const nextContent = getDisabledHermesCommandsPluginConfigContent({
@@ -20417,6 +20664,11 @@ var CommandsProcessor = class extends FeatureProcessor {
20417
20664
  if (this.toolTarget !== "goose" || !this.global) return 0;
20418
20665
  if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
20419
20666
  const configPath = join(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
20667
+ if (await refusesWriteOutsideRoot({
20668
+ logger: this.logger,
20669
+ rootPath: this.outputRoot,
20670
+ targetPath: configPath
20671
+ })) return 0;
20420
20672
  const currentContent = await readFileContentOrNull(configPath);
20421
20673
  if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
20422
20674
  const nextContent = getGooseSlashCommandsConfigContent({
@@ -27516,6 +27768,302 @@ var PiHooks = class PiHooks extends ToolHooks {
27516
27768
  }
27517
27769
  };
27518
27770
  //#endregion
27771
+ //#region src/constants/pool-paths.ts
27772
+ /**
27773
+ * Pool (Poolside's coding agent CLI) configuration-layout conventions.
27774
+ *
27775
+ * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
27776
+ * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
27777
+ * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
27778
+ * path), the project-root `AGENTS.md`, and nested per-directory
27779
+ * `AGENTS.md` files from the repository root down through the working
27780
+ * directory, deeper files taking precedence. It skips ignored directories
27781
+ * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
27782
+ *
27783
+ * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
27784
+ * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
27785
+ * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
27786
+ * the skill directories of other Agent Skills tools; rulesync writes only the
27787
+ * Pool-specific roots and leaves the shared ones to their own targets.
27788
+ *
27789
+ * @see https://docs.poolside.ai/agent-instructions
27790
+ * @see https://docs.poolside.ai/skills
27791
+ * @see https://github.com/poolsideai/pool
27792
+ */
27793
+ /** Project-scoped `.poolside/` directory at the project root. */
27794
+ const POOL_DIR = ".poolside";
27795
+ /** Global config directory for Pool, relative to the home directory. */
27796
+ const POOL_GLOBAL_DIR = join(".config", "poolside");
27797
+ /** Project skills root, relative to the project root. */
27798
+ const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
27799
+ /** Global skills root, relative to the home directory. */
27800
+ const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
27801
+ /**
27802
+ * Pool's settings file name. MCP servers live under the top-level
27803
+ * `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
27804
+ * `~/.config/poolside/settings.yaml` (global); the untracked
27805
+ * `.poolside/settings.local.yaml` overlay is left to the user.
27806
+ *
27807
+ * @see https://docs.poolside.ai/mcp-servers
27808
+ * @see https://docs.poolside.ai/settings-file-reference
27809
+ */
27810
+ const POOL_SETTINGS_FILE_NAME = "settings.yaml";
27811
+ /** Pool's untracked per-user overlay next to the settings file; never written. */
27812
+ const POOL_SETTINGS_LOCAL_FILE_NAME = "settings.local.yaml";
27813
+ /** Top-level key of Pool's settings file that holds the MCP server map. */
27814
+ const POOL_MCP_SERVERS_KEY = "mcp_servers";
27815
+ /** Top-level key of Pool's settings file that holds per-tool permission rules. */
27816
+ const POOL_TOOLS_KEY = "tools";
27817
+ /** Top-level key of Pool's settings file that holds file access rules. */
27818
+ const POOL_PATHS_KEY = "paths";
27819
+ /** Top-level key of Pool's settings file that holds the per-event hook lists. */
27820
+ const POOL_HOOKS_KEY = "hooks";
27821
+ /**
27822
+ * Top-level key of Pool's settings file that holds the subagent registry:
27823
+ * `subagents.default` names the agent delegated to by default and
27824
+ * `subagents.agents.<name>` defines each agent.
27825
+ *
27826
+ * @see https://docs.poolside.ai/subagents
27827
+ */
27828
+ const POOL_SUBAGENTS_KEY = "subagents";
27829
+ /** Key under `subagents` that holds the per-agent definitions. */
27830
+ const POOL_SUBAGENTS_AGENTS_KEY = "agents";
27831
+ /** Pool's reserved built-in agent, always `in_process` and never generated. */
27832
+ const POOL_GENERAL_AGENT_NAME = "general";
27833
+ //#endregion
27834
+ //#region src/features/hooks/pool-hooks.ts
27835
+ /** The matcher Pool reads as "any tool"; `""` means the same. */
27836
+ const POOL_MATCH_ALL = "*";
27837
+ const SUPPORTED_POOL_EVENTS = new Set(POOL_HOOK_EVENTS);
27838
+ /**
27839
+ * The Pool events that test `matcher` against a tool name. Pool ignores the
27840
+ * field on every other event (it stays required by the schema), so an
27841
+ * authored matcher there is dropped with a warning and `*` written instead.
27842
+ */
27843
+ const POOL_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
27844
+ /**
27845
+ * Single spelling of the settings.yaml codec/policy, matching PoolMcp and
27846
+ * PoolPermissions: fail closed on an unparseable root rather than replacing
27847
+ * the user's primary Pool settings with generated output.
27848
+ */
27849
+ function parsePoolSettings$3(fileContent, filePath) {
27850
+ return parseSharedConfig({
27851
+ format: "yaml",
27852
+ fileContent,
27853
+ filePath,
27854
+ invalidRootPolicy: "error"
27855
+ });
27856
+ }
27857
+ /**
27858
+ * Build the `hooks` event lists of Pool's settings file from a canonical hooks
27859
+ * config. Pool keys a flat list of `{name, matcher, command, timeout}` entries
27860
+ * by event name. Only `type: "command"` canonical hooks are emitted, since a
27861
+ * Pool hook is a shell command. A shared canonical event Pool does not fire is
27862
+ * skipped (the HooksProcessor reports it), while an event under the
27863
+ * `pool.hooks` override — such as one an import filed there — is written
27864
+ * verbatim so it round-trips.
27865
+ */
27866
+ function canonicalToPoolHooks({ config, toolOverride, logger }) {
27867
+ const sharedHooks = {};
27868
+ for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_POOL_EVENTS.has(event)) sharedHooks[event] = defs;
27869
+ const effective = {
27870
+ ...sharedHooks,
27871
+ ...toolOverride
27872
+ };
27873
+ const hooks = {};
27874
+ for (const [event, defs] of Object.entries(effective)) {
27875
+ if (isPrototypePollutionKey(event)) continue;
27876
+ const poolEvent = lookupOwn({
27877
+ record: CANONICAL_TO_POOL_EVENT_NAMES,
27878
+ key: event
27879
+ }) ?? event;
27880
+ const entries = [];
27881
+ for (const def of defs) {
27882
+ const entry = canonicalDefToPoolEntry({
27883
+ def,
27884
+ event,
27885
+ poolEvent,
27886
+ logger
27887
+ });
27888
+ if (entry !== null) entries.push(entry);
27889
+ }
27890
+ if (entries.length > 0) hooks[poolEvent] = entries;
27891
+ }
27892
+ return hooks;
27893
+ }
27894
+ /** Convert one canonical hook definition to a Pool entry, or null to skip. */
27895
+ function canonicalDefToPoolEntry({ def, event, poolEvent, logger }) {
27896
+ if ((def.type ?? "command") !== "command") return null;
27897
+ if (typeof def.command !== "string" || def.command === "") {
27898
+ logger?.warn(`Pool hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Pool would reject, so it was skipped.`);
27899
+ return null;
27900
+ }
27901
+ const authoredMatcher = typeof def.matcher === "string" && def.matcher !== "" ? def.matcher : POOL_MATCH_ALL;
27902
+ let matcher = authoredMatcher;
27903
+ if (!POOL_MATCHER_EVENTS.has(poolEvent) && authoredMatcher !== POOL_MATCH_ALL) {
27904
+ logger?.warn(`Pool ignores "matcher" on "${poolEvent}", so the matcher ${quoteValueForWarning(authoredMatcher)} of the hook ${quoteValueForWarning(def.name ?? def.command)} under "${event}" was dropped.`);
27905
+ matcher = POOL_MATCH_ALL;
27906
+ }
27907
+ const entry = {
27908
+ ...typeof def.name === "string" && def.name !== "" ? { name: def.name } : {},
27909
+ matcher,
27910
+ command: def.command
27911
+ };
27912
+ if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
27913
+ return entry;
27914
+ }
27915
+ /** Convert one raw hook entry to a canonical definition, or null to skip. */
27916
+ function poolEntryToCanonicalDef({ raw, poolEvent }) {
27917
+ if (!isRecord$1(raw) || typeof raw.command !== "string" || raw.command === "") return null;
27918
+ const def = {
27919
+ type: "command",
27920
+ command: raw.command
27921
+ };
27922
+ if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
27923
+ if (POOL_MATCHER_EVENTS.has(poolEvent) && typeof raw.matcher === "string" && raw.matcher !== "" && raw.matcher !== POOL_MATCH_ALL) def.matcher = raw.matcher;
27924
+ if (typeof raw.timeout === "number") def.timeout = raw.timeout;
27925
+ return def;
27926
+ }
27927
+ /**
27928
+ * Reverse {@link canonicalToPoolHooks}: parse the event lists of the `hooks`
27929
+ * block back into a canonical event → definition[] record. An event Pool does
27930
+ * not document is carried under its own name so `buildImportedHooksConfig`
27931
+ * files it under the `pool.hooks` override; a non-list sibling such as
27932
+ * `stop_hook_max_continuations` is not a hook and is left alone.
27933
+ */
27934
+ function poolHooksToCanonical(hooksBlock) {
27935
+ const canonical = {};
27936
+ if (!isRecord$1(hooksBlock)) return canonical;
27937
+ for (const [poolEvent, rawEntries] of Object.entries(hooksBlock)) {
27938
+ if (isPrototypePollutionKey(poolEvent) || !Array.isArray(rawEntries)) continue;
27939
+ const canonicalEvent = lookupOwn({
27940
+ record: POOL_TO_CANONICAL_EVENT_NAMES,
27941
+ key: poolEvent
27942
+ }) ?? poolEvent;
27943
+ const defs = rawEntries.map((raw) => poolEntryToCanonicalDef({
27944
+ raw,
27945
+ poolEvent
27946
+ })).filter((def) => def !== null);
27947
+ if (defs.length === 0) continue;
27948
+ canonical[canonicalEvent] = [...lookupOwn({
27949
+ record: canonical,
27950
+ key: canonicalEvent
27951
+ }) ?? [], ...defs];
27952
+ }
27953
+ return canonical;
27954
+ }
27955
+ /**
27956
+ * The keys of an existing `hooks` block that are not event lists —
27957
+ * `stop_hook_max_continuations` and anything else Pool may add beside the
27958
+ * events — which survive a regenerate untouched.
27959
+ */
27960
+ function nonEventHookKeys(hooksBlock) {
27961
+ const kept = {};
27962
+ if (!isRecord$1(hooksBlock)) return kept;
27963
+ for (const [key, value] of Object.entries(hooksBlock)) if (!isPrototypePollutionKey(key) && !Array.isArray(value)) kept[key] = value;
27964
+ return kept;
27965
+ }
27966
+ /**
27967
+ * Pool hooks.
27968
+ *
27969
+ * Pool reads hooks from the `hooks` key of its settings file —
27970
+ * `.poolside/settings.yaml` at project scope and
27971
+ * `~/.config/poolside/settings.yaml` at user scope — as
27972
+ * `hooks.<Event>: [{name?, matcher, command, timeout?}]` for `PreToolUse`,
27973
+ * `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `SessionStart` and `Stop`.
27974
+ * The event lists are rewritten as a whole; the block's non-list sibling
27975
+ * `stop_hook_max_continuations` and every other top-level key of the file
27976
+ * (`mcp_servers`, `tools`, `paths`, `pool`, ...) are preserved, and the file
27977
+ * is never deleted.
27978
+ *
27979
+ * @see https://docs.poolside.ai/hooks
27980
+ * @see https://docs.poolside.ai/settings-file-reference
27981
+ */
27982
+ var PoolHooks = class PoolHooks extends ToolHooks {
27983
+ settings;
27984
+ constructor(params) {
27985
+ super(params);
27986
+ this.settings = parsePoolSettings$3(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
27987
+ }
27988
+ getSettings() {
27989
+ return this.settings;
27990
+ }
27991
+ isDeletable() {
27992
+ return false;
27993
+ }
27994
+ static getSettablePaths({ global = false } = {}) {
27995
+ return {
27996
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
27997
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
27998
+ };
27999
+ }
28000
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
28001
+ const paths = this.getSettablePaths({ global });
28002
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
28003
+ return new PoolHooks({
28004
+ outputRoot,
28005
+ relativeDirPath: paths.relativeDirPath,
28006
+ relativeFilePath: paths.relativeFilePath,
28007
+ fileContent,
28008
+ validate,
28009
+ global
28010
+ });
28011
+ }
28012
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
28013
+ const paths = this.getSettablePaths({ global });
28014
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
28015
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
28016
+ const existing = parsePoolSettings$3(existingContent, filePath);
28017
+ const config = rulesyncHooks.getJson();
28018
+ const hooks = canonicalToPoolHooks({
28019
+ config,
28020
+ toolOverride: config.pool?.hooks,
28021
+ logger
28022
+ });
28023
+ return new PoolHooks({
28024
+ outputRoot,
28025
+ relativeDirPath: paths.relativeDirPath,
28026
+ relativeFilePath: paths.relativeFilePath,
28027
+ fileContent: applySharedConfigPatch({
28028
+ fileKey: sharedConfigFileKey(paths),
28029
+ feature: "hooks",
28030
+ existingContent,
28031
+ patch: { [POOL_HOOKS_KEY]: {
28032
+ ...nonEventHookKeys(existing[POOL_HOOKS_KEY]),
28033
+ ...hooks
28034
+ } },
28035
+ filePath,
28036
+ logger
28037
+ }),
28038
+ validate,
28039
+ global
28040
+ });
28041
+ }
28042
+ toRulesyncHooks() {
28043
+ const hooks = poolHooksToCanonical(this.settings[POOL_HOOKS_KEY]);
28044
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
28045
+ hooks,
28046
+ overrideKey: "pool"
28047
+ }), null, 2) });
28048
+ }
28049
+ validate() {
28050
+ return {
28051
+ success: true,
28052
+ error: null
28053
+ };
28054
+ }
28055
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
28056
+ return new PoolHooks({
28057
+ outputRoot,
28058
+ relativeDirPath,
28059
+ relativeFilePath,
28060
+ fileContent: "",
28061
+ validate: false,
28062
+ global
28063
+ });
28064
+ }
28065
+ };
28066
+ //#endregion
27519
28067
  //#region src/features/hooks/qwencode-hooks.ts
27520
28068
  /**
27521
28069
  * Build a single Qwen Code hook object from a canonical hook definition.
@@ -28629,14 +29177,15 @@ const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
28629
29177
  /**
28630
29178
  * Event names present in the config that the target's adapter cannot emit.
28631
29179
  *
28632
- * When the factory passes override-block keys through verbatim
28633
- * (`passthroughOverrideEvents`), those keys are excluded from the check so
28634
- * documented passthrough triggers aren't falsely reported as skipped.
29180
+ * Shared (`hooks`) keys are always checked. Override-block keys are only
29181
+ * checked when the factory declares `dropsUnknownOverrideEvents`; otherwise
29182
+ * the adapter either emits them verbatim or reports its own drops, and a
29183
+ * generic "not supported" warning would be wrong or duplicated.
28635
29184
  */
28636
29185
  function unsupportedEventNames(params) {
28637
29186
  const { factory, sharedHooks, effectiveHooks } = params;
28638
29187
  const supportedEvents = new Set(factory.supportedEvents);
28639
- const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
29188
+ const eventNames = factory.dropsUnknownOverrideEvents ? Object.keys(effectiveHooks) : Object.keys(sharedHooks);
28640
29189
  return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
28641
29190
  }
28642
29191
  /**
@@ -28729,7 +29278,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28729
29278
  supportedEvents: AMP_HOOK_EVENTS,
28730
29279
  supportedHookTypes: ["command"],
28731
29280
  supportsMatcher: true,
28732
- matcherEvents: ["preToolUse", "postToolUse"]
29281
+ matcherEvents: ["preToolUse", "postToolUse"],
29282
+ dropsUnknownOverrideEvents: true
28733
29283
  }],
28734
29284
  ["antigravity-cli", {
28735
29285
  class: AntigravityCliHooks,
@@ -28762,7 +29312,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28762
29312
  },
28763
29313
  supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
28764
29314
  supportedHookTypes: ["command"],
28765
- supportsMatcher: true
29315
+ supportsMatcher: true,
29316
+ dropsUnknownOverrideEvents: true
28766
29317
  }],
28767
29318
  ["cursor", {
28768
29319
  class: CursorHooks,
@@ -28807,7 +29358,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28807
29358
  "mcp_tool",
28808
29359
  "agent"
28809
29360
  ],
28810
- supportsMatcher: true
29361
+ supportsMatcher: true,
29362
+ dropsUnknownOverrideEvents: true
28811
29363
  }],
28812
29364
  ["codexcli", {
28813
29365
  class: CodexcliHooks,
@@ -28868,7 +29420,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28868
29420
  supportedEvents: KILO_HOOK_EVENTS,
28869
29421
  supportedHookTypes: ["command"],
28870
29422
  supportsMatcher: true,
28871
- matcherEvents: ["preToolUse", "postToolUse"]
29423
+ matcherEvents: ["preToolUse", "postToolUse"],
29424
+ dropsUnknownOverrideEvents: true
28872
29425
  }],
28873
29426
  ["opencode", {
28874
29427
  class: OpencodeHooks,
@@ -28880,7 +29433,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28880
29433
  supportedEvents: OPENCODE_HOOK_EVENTS,
28881
29434
  supportedHookTypes: ["command"],
28882
29435
  supportsMatcher: true,
28883
- matcherEvents: ["preToolUse", "postToolUse"]
29436
+ matcherEvents: ["preToolUse", "postToolUse"],
29437
+ dropsUnknownOverrideEvents: true
28884
29438
  }],
28885
29439
  ["pi", {
28886
29440
  class: PiHooks,
@@ -28891,7 +29445,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28891
29445
  },
28892
29446
  supportedEvents: PI_HOOK_EVENTS,
28893
29447
  supportedHookTypes: ["command"],
28894
- supportsMatcher: true
29448
+ supportsMatcher: true,
29449
+ dropsUnknownOverrideEvents: true
28895
29450
  }],
28896
29451
  ["factorydroid", {
28897
29452
  class: FactorydroidHooks,
@@ -28913,7 +29468,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28913
29468
  },
28914
29469
  supportedEvents: CLINE_HOOK_EVENTS,
28915
29470
  supportedHookTypes: ["command"],
28916
- supportsMatcher: false
29471
+ supportsMatcher: false,
29472
+ dropsUnknownOverrideEvents: true
28917
29473
  }],
28918
29474
  ["goose", {
28919
29475
  class: GooseHooks,
@@ -28935,8 +29491,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28935
29491
  },
28936
29492
  supportedEvents: HERMESAGENT_HOOK_EVENTS,
28937
29493
  supportedHookTypes: ["command"],
28938
- supportsMatcher: true,
28939
- passthroughOverrideEvents: true
29494
+ supportsMatcher: true
28940
29495
  }],
28941
29496
  ["kimi-code", {
28942
29497
  class: KimiCodeHooks,
@@ -28947,8 +29502,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28947
29502
  },
28948
29503
  supportedEvents: KIMI_CODE_HOOK_EVENTS,
28949
29504
  supportedHookTypes: ["command"],
28950
- supportsMatcher: true,
28951
- passthroughOverrideEvents: true
29505
+ supportsMatcher: true
28952
29506
  }],
28953
29507
  ["deepagents", {
28954
29508
  class: DeepagentsHooks,
@@ -28959,7 +29513,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28959
29513
  },
28960
29514
  supportedEvents: DEEPAGENTS_HOOK_EVENTS,
28961
29515
  supportedHookTypes: ["command"],
28962
- supportsMatcher: true
29516
+ supportsMatcher: true,
29517
+ dropsUnknownOverrideEvents: true
28963
29518
  }],
28964
29519
  ["kiro", {
28965
29520
  class: KiroHooks,
@@ -28970,8 +29525,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28970
29525
  },
28971
29526
  supportedEvents: KIRO_HOOK_EVENTS,
28972
29527
  supportedHookTypes: ["command"],
28973
- supportsMatcher: true,
28974
- passthroughOverrideEvents: true
29528
+ supportsMatcher: true
28975
29529
  }],
28976
29530
  ["kiro-cli", {
28977
29531
  class: KiroCliHooks,
@@ -28982,8 +29536,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28982
29536
  },
28983
29537
  supportedEvents: KIRO_IDE_HOOK_EVENTS,
28984
29538
  supportedHookTypes: ["command", "prompt"],
28985
- supportsMatcher: true,
28986
- passthroughOverrideEvents: true
29539
+ supportsMatcher: true
28987
29540
  }],
28988
29541
  ["kiro-ide", {
28989
29542
  class: KiroIdeHooks,
@@ -28994,8 +29547,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28994
29547
  },
28995
29548
  supportedEvents: KIRO_IDE_HOOK_EVENTS,
28996
29549
  supportedHookTypes: ["command", "prompt"],
28997
- supportsMatcher: true,
28998
- passthroughOverrideEvents: true
29550
+ supportsMatcher: true
28999
29551
  }],
29000
29552
  ["devin", {
29001
29553
  class: DevinHooks,
@@ -29113,6 +29665,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29113
29665
  },
29114
29666
  supportedEvents: VIBE_HOOK_EVENTS,
29115
29667
  supportedHookTypes: ["command"],
29668
+ supportsMatcher: true,
29669
+ dropsUnknownOverrideEvents: true
29670
+ }],
29671
+ ["pool", {
29672
+ class: PoolHooks,
29673
+ meta: {
29674
+ supportsProject: true,
29675
+ supportsGlobal: true,
29676
+ supportsImport: true
29677
+ },
29678
+ supportedEvents: POOL_HOOK_EVENTS,
29679
+ supportedHookTypes: ["command"],
29116
29680
  supportsMatcher: true
29117
29681
  }],
29118
29682
  ["qwencode", {
@@ -29140,7 +29704,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29140
29704
  },
29141
29705
  supportedEvents: REASONIX_HOOK_EVENTS,
29142
29706
  supportedHookTypes: ["command"],
29143
- supportsMatcher: true
29707
+ supportsMatcher: true,
29708
+ dropsUnknownOverrideEvents: true
29144
29709
  }],
29145
29710
  ["grokcli", {
29146
29711
  class: GrokcliHooks,
@@ -29162,8 +29727,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29162
29727
  },
29163
29728
  supportedEvents: CRUSH_HOOK_EVENTS,
29164
29729
  supportedHookTypes: ["command"],
29165
- supportsMatcher: true,
29166
- passthroughOverrideEvents: true
29730
+ supportsMatcher: true
29167
29731
  }],
29168
29732
  ["zcode", {
29169
29733
  class: ZcodeHooks,
@@ -35797,7 +36361,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
35797
36361
  * that is not an object at all costs nothing.
35798
36362
  */
35799
36363
  function readExistingKiloMcpEntries(fileContent) {
35800
- const mcp = parse(fileContent || "{}")?.mcp;
36364
+ const mcp = parse$1(fileContent || "{}")?.mcp;
35801
36365
  if (!isRecord$1(mcp)) return {};
35802
36366
  const entries = {};
35803
36367
  for (const [serverName, entry] of Object.entries(mcp)) {
@@ -35821,7 +36385,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
35821
36385
  json;
35822
36386
  constructor(params) {
35823
36387
  super(params);
35824
- this.json = KiloConfigSchema.parse(parse(this.fileContent || "{}"));
36388
+ this.json = KiloConfigSchema.parse(parse$1(this.fileContent || "{}"));
35825
36389
  }
35826
36390
  getJson() {
35827
36391
  return this.json;
@@ -35889,7 +36453,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
35889
36453
  outputRoot,
35890
36454
  global
35891
36455
  });
35892
- const json = parse(fileContent ?? "{\"mcp\":{}}");
36456
+ const json = parse$1(fileContent ?? "{\"mcp\":{}}");
35893
36457
  const newJson = {
35894
36458
  ...json,
35895
36459
  mcp: json.mcp ?? {}
@@ -35960,7 +36524,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
35960
36524
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
35961
36525
  }
35962
36526
  if (instructions.length === 0 && fileContent === null) return null;
35963
- const json = fileContent ? parse(fileContent) : {};
36527
+ const json = fileContent ? parse$1(fileContent) : {};
35964
36528
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
35965
36529
  const managedPrefix = `${toPosixPath(join(KILO_DIR, KILO_RULES_DIR_NAME))}/`;
35966
36530
  const preservedInstructions = existingInstructions.filter((entry) => !toPosixPath(entry).replace(/^\.\//, "").startsWith(managedPrefix));
@@ -36924,7 +37488,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
36924
37488
  json;
36925
37489
  constructor(params) {
36926
37490
  super(params);
36927
- this.json = OpencodeConfigSchema.parse(parse(this.fileContent || "{}"));
37491
+ this.json = OpencodeConfigSchema.parse(parse$1(this.fileContent || "{}"));
36928
37492
  }
36929
37493
  getJson() {
36930
37494
  return this.json;
@@ -36957,7 +37521,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
36957
37521
  fileContent = await readFileContentOrNull(jsonPath);
36958
37522
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
36959
37523
  }
36960
- const json = parse(fileContent ?? "{\"mcp\":{}}");
37524
+ const json = parse$1(fileContent ?? "{\"mcp\":{}}");
36961
37525
  const newJson = {
36962
37526
  ...json,
36963
37527
  mcp: json.mcp ?? {}
@@ -36986,7 +37550,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
36986
37550
  mcpServers: rulesyncMcp.getMcpServers(),
36987
37551
  replacement: "{env:$1}"
36988
37552
  });
36989
- const existingMcp = OpencodeConfigSchema.safeParse(parse(fileContent || "{}"));
37553
+ const existingMcp = OpencodeConfigSchema.safeParse(parse$1(fileContent || "{}"));
36990
37554
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
36991
37555
  return new OpencodeMcp({
36992
37556
  outputRoot,
@@ -37037,7 +37601,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
37037
37601
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
37038
37602
  }
37039
37603
  if (instructions.length === 0 && fileContent === null) return null;
37040
- const json = fileContent ? parse(fileContent) : {};
37604
+ const json = fileContent ? parse$1(fileContent) : {};
37041
37605
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
37042
37606
  const managedPrefixes = global ? ["memories/", `${configDirPrefix}memories/`] : [`${toPosixPath(OPENCODE_DIR)}/memories/`];
37043
37607
  const preservedInstructions = existingInstructions.filter((entry) => {
@@ -37094,6 +37658,320 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
37094
37658
  }
37095
37659
  };
37096
37660
  //#endregion
37661
+ //#region src/features/mcp/pool-mcp.ts
37662
+ /**
37663
+ * Single spelling of the settings.yaml codec/policy, matching the
37664
+ * `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
37665
+ * unparseable root rather than replacing the user's primary Pool settings with
37666
+ * generated output.
37667
+ */
37668
+ function parsePoolSettings$2(fileContent, filePath) {
37669
+ return parseSharedConfig({
37670
+ format: "yaml",
37671
+ fileContent,
37672
+ filePath,
37673
+ invalidRootPolicy: "error"
37674
+ });
37675
+ }
37676
+ /**
37677
+ * The remote transports Pool's `transport.type` accepts. `streamable-http` is
37678
+ * the canonical rulesync name for what Pool calls `http`; everything else is
37679
+ * left to the caller to reject.
37680
+ *
37681
+ * @see https://docs.poolside.ai/mcp-servers
37682
+ */
37683
+ function asPoolRemoteType(stated, url) {
37684
+ if (stated === "sse") return "sse";
37685
+ if (stated === "http" || stated === "streamable-http") return "http";
37686
+ if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
37687
+ }
37688
+ /**
37689
+ * Pool spells remote headers as a list of `"Name: value"` strings rather than
37690
+ * a map, so the canonical record is flattened at generate time and split back
37691
+ * at the first `:` on import. A list entry without a `:` names no header and
37692
+ * is dropped with a warning — silently losing what may be a mistyped auth
37693
+ * header would leave the canonical file short of one without a trace.
37694
+ */
37695
+ function headersRecordToPoolList(headers) {
37696
+ return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
37697
+ }
37698
+ function poolHeadersListToRecord(serverName, headers) {
37699
+ if (!isStringArray$2(headers)) return void 0;
37700
+ const result = {};
37701
+ for (const entry of headers) {
37702
+ const separator = entry.indexOf(":");
37703
+ const name = separator > 0 ? entry.slice(0, separator).trim() : "";
37704
+ if (name === "" || PROTOTYPE_POLLUTION_KEYS.has(name)) {
37705
+ warnWithFallback(void 0, `Ignored malformed header ${quoteValueForWarning(entry)} in Pool MCP server ${quoteValueForWarning(serverName)}: expected "Name: value"`);
37706
+ continue;
37707
+ }
37708
+ result[name] = entry.slice(separator + 1).trim();
37709
+ }
37710
+ return Object.keys(result).length > 0 ? result : void 0;
37711
+ }
37712
+ /**
37713
+ * Pool's `deny`/`enabled_tools` entries are glob patterns, while the canonical
37714
+ * `enabledTools`/`disabledTools` lists are literal tool names that the other
37715
+ * targets match verbatim. A pattern therefore imports as a name that matches
37716
+ * nothing elsewhere — a `deny` that quietly stops denying is the case worth
37717
+ * calling out — so it is copied as-is but warned about. `allow` is exempt: it
37718
+ * imports to the Pool-only `poolAllow`, where globs are exactly what Pool
37719
+ * expects and no other tool ever reads them.
37720
+ */
37721
+ const GLOB_METACHARACTERS = /[*?[]/;
37722
+ /** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
37723
+ const MAX_LISTED_GLOB_PATTERNS = 10;
37724
+ function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
37725
+ if (!isStringArray$2(value)) {
37726
+ warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
37727
+ return;
37728
+ }
37729
+ if (!warnOnGlobs) return value;
37730
+ const patterns = value.filter((entry) => GLOB_METACHARACTERS.test(entry));
37731
+ if (patterns.length > 0) {
37732
+ const listed = patterns.slice(0, MAX_LISTED_GLOB_PATTERNS).map(quoteValueForWarning);
37733
+ const overflow = patterns.length - listed.length;
37734
+ const shown = overflow > 0 ? `${listed.join(", ")} (+${overflow} more)` : listed.join(", ");
37735
+ warnWithFallback(void 0, `${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)} contains glob patterns (${shown}); other tools match the imported tool names literally`);
37736
+ }
37737
+ return value;
37738
+ }
37739
+ /**
37740
+ * Pool's `transport` block for a remote server, or `null` (after a warning)
37741
+ * when the server has no url or names a transport Pool cannot reach.
37742
+ */
37743
+ function toPoolTransport(name, config, logger) {
37744
+ const url = resolveRemoteMcpUrl(config);
37745
+ if (!url) return warnAndSkipMcpServer({
37746
+ toolName: "Pool",
37747
+ serverName: name,
37748
+ reason: "a remote transport without a url",
37749
+ logger
37750
+ });
37751
+ const stated = config.type ?? config.transport;
37752
+ const type = asPoolRemoteType(stated, url);
37753
+ if (type === void 0) return warnAndSkipMcpServer({
37754
+ toolName: "Pool",
37755
+ serverName: name,
37756
+ 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)`,
37757
+ logger
37758
+ });
37759
+ const transport = {
37760
+ type,
37761
+ url
37762
+ };
37763
+ if (config.headers && Object.keys(config.headers).length > 0) transport.headers = headersRecordToPoolList(config.headers);
37764
+ return transport;
37765
+ }
37766
+ /**
37767
+ * The transport-specific part of a Pool server entry: a `transport` block for
37768
+ * remote servers, `command`/`args`/`cwd` for stdio ones. `null` (after a
37769
+ * warning) when the server cannot be written.
37770
+ */
37771
+ function toPoolTransportFields(name, config, logger) {
37772
+ if (isRemoteMcpServer(config)) {
37773
+ const transport = toPoolTransport(name, config, logger);
37774
+ return transport === null ? null : { transport };
37775
+ }
37776
+ const [command, ...args] = resolveLocalMcpCommand(config);
37777
+ if (!command) return warnAndSkipMcpServer({
37778
+ toolName: "Pool",
37779
+ serverName: name,
37780
+ reason: "a stdio transport without a command",
37781
+ logger
37782
+ });
37783
+ const fields = {
37784
+ command,
37785
+ args
37786
+ };
37787
+ if (config.cwd) fields.cwd = config.cwd;
37788
+ return fields;
37789
+ }
37790
+ /**
37791
+ * The fields shared by stdio and remote entries. Tool filters map onto Pool's
37792
+ * own switches — `enabledTools` becomes `enabled_tools` (the tools Pool
37793
+ * exposes at all) and `disabledTools` becomes `deny` (patterns agents may
37794
+ * never use); the Pool-only approval allowlist is authored as `poolAllow` and
37795
+ * written as `allow`. `disabled: true` is spelled the same on both sides.
37796
+ */
37797
+ function toPoolCommonFields(config) {
37798
+ const fields = {};
37799
+ if (config.env && Object.keys(config.env).length > 0) fields.env = config.env;
37800
+ if (config.enabledTools && config.enabledTools.length > 0) fields.enabled_tools = config.enabledTools;
37801
+ if (config.poolAllow && config.poolAllow.length > 0) fields.allow = config.poolAllow;
37802
+ if (config.disabledTools && config.disabledTools.length > 0) fields.deny = config.disabledTools;
37803
+ if (config.disabled === true) fields.disabled = true;
37804
+ return fields;
37805
+ }
37806
+ /**
37807
+ * Convert canonical rulesync servers to Pool's native `mcp_servers` shape:
37808
+ * stdio servers carry `command`/`args`/`cwd`/`env`, remote servers carry a
37809
+ * `transport` block (`type` of `http` or `sse`, `url`, optional `headers`
37810
+ * list) plus optional `env`; see `toPoolCommonFields` for the tool filters.
37811
+ *
37812
+ * @see https://docs.poolside.ai/mcp-servers
37813
+ */
37814
+ function convertToPoolFormat(mcpServers, logger) {
37815
+ const result = {};
37816
+ for (const [name, config] of Object.entries(mcpServers)) {
37817
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
37818
+ if (!isRecord$1(config)) continue;
37819
+ if (declaresNoTransport(config)) {
37820
+ warnAndSkipMcpServer({
37821
+ toolName: "Pool",
37822
+ serverName: name,
37823
+ reason: "no transport",
37824
+ logger
37825
+ });
37826
+ continue;
37827
+ }
37828
+ const transportFields = toPoolTransportFields(name, config, logger);
37829
+ if (transportFields === null) continue;
37830
+ result[name] = {
37831
+ ...transportFields,
37832
+ ...toPoolCommonFields(config)
37833
+ };
37834
+ }
37835
+ return result;
37836
+ }
37837
+ const POOL_TO_CANONICAL_TOOL_LIST_KEYS = {
37838
+ enabled_tools: "enabledTools",
37839
+ deny: "disabledTools",
37840
+ allow: "poolAllow"
37841
+ };
37842
+ /**
37843
+ * Convert Pool's native `mcp_servers` shape back to canonical rulesync
37844
+ * servers: the `transport` block is flattened to `type`/`url`/`headers`,
37845
+ * `enabled_tools` maps back to `enabledTools`, `deny` to `disabledTools` and
37846
+ * `allow` to the namespaced `poolAllow` (see `McpServerSchema`). Unknown keys
37847
+ * pass through untouched so an import keeps whatever a hand-authored entry
37848
+ * declared; generation back out is a whitelist, so only the keys
37849
+ * `convertToPoolFormat` writes are re-emitted.
37850
+ */
37851
+ function convertFromPoolFormat(poolServers) {
37852
+ const result = {};
37853
+ for (const [name, config] of Object.entries(poolServers)) {
37854
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
37855
+ const converted = {};
37856
+ for (const [key, value] of Object.entries(config)) {
37857
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
37858
+ if (key === "transport") {
37859
+ if (!isRecord$1(value)) continue;
37860
+ if (typeof value.type === "string") converted.type = value.type;
37861
+ if (typeof value.url === "string") converted.url = value.url;
37862
+ const headers = poolHeadersListToRecord(name, value.headers);
37863
+ if (headers) converted.headers = headers;
37864
+ continue;
37865
+ }
37866
+ const canonicalKey = Object.hasOwn(POOL_TO_CANONICAL_TOOL_LIST_KEYS, key) ? POOL_TO_CANONICAL_TOOL_LIST_KEYS[key] : void 0;
37867
+ if (canonicalKey) {
37868
+ const list = importPoolToolList({
37869
+ serverName: name,
37870
+ poolKey: key,
37871
+ value,
37872
+ warnOnGlobs: canonicalKey !== "poolAllow"
37873
+ });
37874
+ if (list) converted[canonicalKey] = list;
37875
+ continue;
37876
+ }
37877
+ converted[key] = value;
37878
+ }
37879
+ result[name] = converted;
37880
+ }
37881
+ return result;
37882
+ }
37883
+ /**
37884
+ * Pool MCP servers.
37885
+ *
37886
+ * Pool reads MCP servers from the `mcp_servers` block of its settings file:
37887
+ * `<project>/.poolside/settings.yaml` at project scope (shared and committed)
37888
+ * and `~/.config/poolside/settings.yaml` at user scope. The untracked
37889
+ * `.poolside/settings.local.yaml` overlay, which Pool layers on top of both,
37890
+ * is left to the user. Every other top-level settings key is preserved and
37891
+ * the file is never deleted.
37892
+ *
37893
+ * @see https://docs.poolside.ai/mcp-servers
37894
+ * @see https://docs.poolside.ai/settings-file-reference
37895
+ */
37896
+ var PoolMcp = class PoolMcp extends ToolMcp {
37897
+ settings;
37898
+ constructor(params) {
37899
+ super(params);
37900
+ this.settings = parsePoolSettings$2(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
37901
+ }
37902
+ getSettings() {
37903
+ return this.settings;
37904
+ }
37905
+ isDeletable() {
37906
+ return false;
37907
+ }
37908
+ static getSettablePaths({ global = false } = {}) {
37909
+ return {
37910
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
37911
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
37912
+ };
37913
+ }
37914
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
37915
+ const paths = this.getSettablePaths({ global });
37916
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
37917
+ return new PoolMcp({
37918
+ outputRoot,
37919
+ relativeDirPath: paths.relativeDirPath,
37920
+ relativeFilePath: paths.relativeFilePath,
37921
+ fileContent,
37922
+ validate,
37923
+ global
37924
+ });
37925
+ }
37926
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
37927
+ const paths = this.getSettablePaths({ global });
37928
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
37929
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
37930
+ const converted = convertToPoolFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
37931
+ const rawServer = rulesyncMcp.getRawMcpServer(serverName);
37932
+ const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
37933
+ return [serverName, {
37934
+ ...serverConfig,
37935
+ ...isStringArray$2(poolAllow) && { poolAllow }
37936
+ }];
37937
+ })), logger);
37938
+ return new PoolMcp({
37939
+ outputRoot,
37940
+ relativeDirPath: paths.relativeDirPath,
37941
+ relativeFilePath: paths.relativeFilePath,
37942
+ fileContent: applySharedConfigPatch({
37943
+ fileKey: sharedConfigFileKey(paths),
37944
+ feature: "mcp",
37945
+ existingContent,
37946
+ patch: { [POOL_MCP_SERVERS_KEY]: converted },
37947
+ filePath
37948
+ }),
37949
+ validate,
37950
+ global
37951
+ });
37952
+ }
37953
+ toRulesyncMcp() {
37954
+ const converted = convertFromPoolFormat(isRecord$1(this.settings["mcp_servers"]) ? this.settings[POOL_MCP_SERVERS_KEY] : {});
37955
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
37956
+ }
37957
+ validate() {
37958
+ return {
37959
+ success: true,
37960
+ error: null
37961
+ };
37962
+ }
37963
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
37964
+ return new PoolMcp({
37965
+ outputRoot,
37966
+ relativeDirPath,
37967
+ relativeFilePath,
37968
+ fileContent: "",
37969
+ validate: false,
37970
+ global
37971
+ });
37972
+ }
37973
+ };
37974
+ //#endregion
37097
37975
  //#region src/features/mcp/qwencode-mcp.ts
37098
37976
  /**
37099
37977
  * Map rulesync's canonical per-server tool filters to Qwen Code's field names.
@@ -39541,6 +40419,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
39541
40419
  supportsDisabledTools: true
39542
40420
  }
39543
40421
  }],
40422
+ ["pool", {
40423
+ class: PoolMcp,
40424
+ meta: {
40425
+ supportsProject: true,
40426
+ supportsGlobal: true,
40427
+ supportsEnabledTools: true,
40428
+ supportsDisabledTools: true
40429
+ }
40430
+ }],
39544
40431
  ["qwencode", {
39545
40432
  class: QwencodeMcp,
39546
40433
  meta: {
@@ -40854,7 +41741,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
40854
41741
  function toAntigravityCliToolName(canonical) {
40855
41742
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
40856
41743
  }
40857
- function toCanonicalToolName$8(cliName) {
41744
+ function toCanonicalToolName$9(cliName) {
40858
41745
  return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
40859
41746
  }
40860
41747
  /**
@@ -41070,7 +41957,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
41070
41957
  const processEntries = (entries, action) => {
41071
41958
  for (const entry of entries) {
41072
41959
  const { toolName, pattern } = parsePermissionEntry$1(entry);
41073
- const canonical = toCanonicalToolName$8(toolName);
41960
+ const canonical = toCanonicalToolName$9(toolName);
41074
41961
  if (!permission[canonical]) permission[canonical] = {};
41075
41962
  permission[canonical][pattern] = action;
41076
41963
  }
@@ -41349,7 +42236,7 @@ function toLegacyAugmentToolName(augmentName) {
41349
42236
  key: augmentName
41350
42237
  }) ?? augmentName;
41351
42238
  }
41352
- function toCanonicalToolName$7(augmentName) {
42239
+ function toCanonicalToolName$8(augmentName) {
41353
42240
  const legacyName = toLegacyAugmentToolName(augmentName);
41354
42241
  return lookupOwn({
41355
42242
  record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
@@ -41802,7 +42689,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
41802
42689
  const type = entry.permission.type;
41803
42690
  if (!isBasicAugmentType(type)) continue;
41804
42691
  const legacyToolName = toLegacyAugmentToolName(entry.toolName);
41805
- const canonical = toCanonicalToolName$7(entry.toolName);
42692
+ const canonical = toCanonicalToolName$8(entry.toolName);
41806
42693
  if (forbiddenMapKeys.has(canonical)) {
41807
42694
  logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
41808
42695
  continue;
@@ -42047,7 +42934,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
42047
42934
  function toClaudeToolName(canonical) {
42048
42935
  return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
42049
42936
  }
42050
- function toCanonicalToolName$6(claudeName) {
42937
+ function toCanonicalToolName$7(claudeName) {
42051
42938
  return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
42052
42939
  }
42053
42940
  /**
@@ -42961,7 +43848,7 @@ function convertClaudeToRulesyncPermissions(params) {
42961
43848
  const processEntries = (entries, action) => {
42962
43849
  for (const entry of entries) {
42963
43850
  const { toolName, pattern } = parseClaudePermissionEntry(entry);
42964
- const canonical = toCanonicalToolName$6(toolName);
43851
+ const canonical = toCanonicalToolName$7(toolName);
42965
43852
  if (!permission[canonical]) permission[canonical] = {};
42966
43853
  permission[canonical][pattern] = action;
42967
43854
  }
@@ -43822,8 +44709,8 @@ function mapBashActionToDecision(action) {
43822
44709
  //#region src/features/permissions/commandcode-permissions.ts
43823
44710
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
43824
44711
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
43825
- const CATCH_ALL_PATTERN$6 = "*";
43826
- const MCP_CANONICAL_PREFIX$5 = "mcp__";
44712
+ const CATCH_ALL_PATTERN$7 = "*";
44713
+ const MCP_CANONICAL_PREFIX$6 = "mcp__";
43827
44714
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
43828
44715
  const CATEGORY_TO_COMMANDCODE_TOOL = {
43829
44716
  bash: "Shell",
@@ -43877,10 +44764,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
43877
44764
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
43878
44765
  */
43879
44766
  function buildCommandcodeRule(category, pattern) {
43880
- const catchAll = pattern === CATCH_ALL_PATTERN$6 || pattern === "";
43881
- if (category.startsWith(MCP_CANONICAL_PREFIX$5)) return catchAll ? category : `${category}(${pattern})`;
43882
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$5}${pattern}`;
43883
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$6 : null;
44767
+ const catchAll = pattern === CATCH_ALL_PATTERN$7 || pattern === "";
44768
+ if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
44769
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
44770
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$7 : null;
43884
44771
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
43885
44772
  if (tool === void 0) return null;
43886
44773
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -43916,7 +44803,7 @@ function splitCommandcodeRule(rule) {
43916
44803
  }
43917
44804
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
43918
44805
  function isCatchAllSpecifier(inner) {
43919
- return inner === "" || inner === CATCH_ALL_PATTERN$6;
44806
+ return inner === "" || inner === CATCH_ALL_PATTERN$7;
43920
44807
  }
43921
44808
  /**
43922
44809
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -43950,34 +44837,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
43950
44837
  if (server.length === 0) return null;
43951
44838
  const everyServer = {
43952
44839
  category: "mcp",
43953
- pattern: CATCH_ALL_PATTERN$6,
44840
+ pattern: CATCH_ALL_PATTERN$7,
43954
44841
  notInAllow: true
43955
44842
  };
43956
- if (tool.startsWith(MCP_CANONICAL_PREFIX$5)) {
43957
- if (server === CATCH_ALL_PATTERN$6) return everyServer;
43958
- if (server.includes(CATCH_ALL_PATTERN$6)) return null;
44843
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
44844
+ if (server === CATCH_ALL_PATTERN$7) return everyServer;
44845
+ if (server.includes(CATCH_ALL_PATTERN$7)) return null;
43959
44846
  return {
43960
- category: rest === "" || rest === CATCH_ALL_PATTERN$6 ? `${MCP_CANONICAL_PREFIX$5}${server}` : `${MCP_CANONICAL_PREFIX$5}${remainder}`,
43961
- pattern: CATCH_ALL_PATTERN$6,
44847
+ category: rest === "" || rest === CATCH_ALL_PATTERN$7 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
44848
+ pattern: CATCH_ALL_PATTERN$7,
43962
44849
  notInAllow: false
43963
44850
  };
43964
44851
  }
43965
- const category = `${MCP_CANONICAL_PREFIX$5}${remainder}`;
43966
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$6);
44852
+ const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
44853
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$7);
43967
44854
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
43968
44855
  category,
43969
44856
  pattern: inner,
43970
44857
  notInAllow: true
43971
44858
  } : null;
43972
- if (remainder === CATCH_ALL_PATTERN$6) return everyServer;
43973
- if (rest === CATCH_ALL_PATTERN$6 && !server.includes(CATCH_ALL_PATTERN$6)) return {
43974
- category: `${MCP_CANONICAL_PREFIX$5}${server}`,
43975
- pattern: CATCH_ALL_PATTERN$6,
44859
+ if (remainder === CATCH_ALL_PATTERN$7) return everyServer;
44860
+ if (rest === CATCH_ALL_PATTERN$7 && !server.includes(CATCH_ALL_PATTERN$7)) return {
44861
+ category: `${MCP_CANONICAL_PREFIX$6}${server}`,
44862
+ pattern: CATCH_ALL_PATTERN$7,
43976
44863
  notInAllow: true
43977
44864
  };
43978
44865
  return namesTool ? {
43979
44866
  category,
43980
- pattern: CATCH_ALL_PATTERN$6,
44867
+ pattern: CATCH_ALL_PATTERN$7,
43981
44868
  notInAllow: false
43982
44869
  } : null;
43983
44870
  }
@@ -44001,14 +44888,14 @@ function friendlyToolRule({ category, inner }) {
44001
44888
  if (category === "bash") {
44002
44889
  if (isCatchAllSpecifier(inner)) return {
44003
44890
  category,
44004
- pattern: CATCH_ALL_PATTERN$6,
44891
+ pattern: CATCH_ALL_PATTERN$7,
44005
44892
  notInAllow: false
44006
44893
  };
44007
44894
  const normalized = inner.trim().replace(/\s+/g, " ");
44008
44895
  if (normalized === "" || normalized === ":*") return null;
44009
44896
  return isCatchAllSpecifier(normalized) ? {
44010
44897
  category,
44011
- pattern: CATCH_ALL_PATTERN$6,
44898
+ pattern: CATCH_ALL_PATTERN$7,
44012
44899
  notInAllow: true
44013
44900
  } : {
44014
44901
  category,
@@ -44018,7 +44905,7 @@ function friendlyToolRule({ category, inner }) {
44018
44905
  }
44019
44906
  if (isCatchAllSpecifier(inner)) return {
44020
44907
  category,
44021
- pattern: CATCH_ALL_PATTERN$6,
44908
+ pattern: CATCH_ALL_PATTERN$7,
44022
44909
  notInAllow: false
44023
44910
  };
44024
44911
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -44037,13 +44924,13 @@ function friendlyToolRule({ category, inner }) {
44037
44924
  */
44038
44925
  function parseCommandcodeRule(rule) {
44039
44926
  const { tool, inner } = splitCommandcodeRule(rule);
44040
- if (tool === CATCH_ALL_PATTERN$6) return isCatchAllSpecifier(inner) ? {
44927
+ if (tool === CATCH_ALL_PATTERN$7) return isCatchAllSpecifier(inner) ? {
44041
44928
  category: "*",
44042
- pattern: CATCH_ALL_PATTERN$6,
44929
+ pattern: CATCH_ALL_PATTERN$7,
44043
44930
  notInAllow: true
44044
44931
  } : null;
44045
44932
  const lowered = tool.toLowerCase();
44046
- if (lowered.startsWith(MCP_CANONICAL_PREFIX$5)) return parseMcpCommandcodeRule({
44933
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$6)) return parseMcpCommandcodeRule({
44047
44934
  tool,
44048
44935
  inner
44049
44936
  });
@@ -44061,12 +44948,12 @@ function parseCommandcodeRule(rule) {
44061
44948
  */
44062
44949
  function isFoldedMcpToolNameRule({ rule, category }) {
44063
44950
  const { tool } = splitCommandcodeRule(rule);
44064
- return category.startsWith(MCP_CANONICAL_PREFIX$5) && !tool.startsWith(MCP_CANONICAL_PREFIX$5);
44951
+ return category.startsWith(MCP_CANONICAL_PREFIX$6) && !tool.startsWith(MCP_CANONICAL_PREFIX$6);
44065
44952
  }
44066
44953
  /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
44067
44954
  function scopedMcpRuleTool(rule) {
44068
44955
  const { tool, inner } = splitCommandcodeRule(rule);
44069
- return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$5) ? tool : null;
44956
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$6) ? tool : null;
44070
44957
  }
44071
44958
  /**
44072
44959
  * The string entries of a Command Code permission list. Command Code reads
@@ -44109,7 +44996,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
44109
44996
  }).filter((rule) => {
44110
44997
  const parsed = parseCommandcodeRule(rule);
44111
44998
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
44112
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$6})`);
44999
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$7})`);
44113
45000
  if (key !== "allow" && isStricterAction({
44114
45001
  action: key,
44115
45002
  existing: replacement
@@ -44166,7 +45053,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
44166
45053
  const ranked = /* @__PURE__ */ new Map();
44167
45054
  const managedCategories = /* @__PURE__ */ new Set();
44168
45055
  for (const [category, rules] of Object.entries(permission)) {
44169
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$6);
45056
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$7);
44170
45057
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
44171
45058
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
44172
45059
  for (const [pattern, action] of Object.entries(rules)) {
@@ -44235,7 +45122,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
44235
45122
  function skippedAllowImportMessage({ rule, category }) {
44236
45123
  const { tool, inner } = splitCommandcodeRule(rule);
44237
45124
  if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
44238
- if (tool.startsWith(MCP_CANONICAL_PREFIX$5) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
45125
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$6) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
44239
45126
  return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
44240
45127
  }
44241
45128
  /**
@@ -44437,7 +45324,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
44437
45324
  //#endregion
44438
45325
  //#region src/features/permissions/continue-permissions.ts
44439
45326
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
44440
- const CATCH_ALL_PATTERN$5 = "*";
45327
+ const CATCH_ALL_PATTERN$6 = "*";
44441
45328
  /**
44442
45329
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
44443
45330
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -44445,7 +45332,7 @@ const CATCH_ALL_PATTERN$5 = "*";
44445
45332
  * the bare entry instead of being skipped as a pattern-specific rule.
44446
45333
  */
44447
45334
  function normalizeCatchAllPattern$1(pattern) {
44448
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
45335
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$6 : pattern;
44449
45336
  }
44450
45337
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
44451
45338
  bash: "Bash",
@@ -44474,7 +45361,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
44474
45361
  function toContinueToolName(canonical) {
44475
45362
  return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
44476
45363
  }
44477
- function toCanonicalToolName$5(continueName) {
45364
+ function toCanonicalToolName$6(continueName) {
44478
45365
  return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
44479
45366
  }
44480
45367
  /**
@@ -44482,7 +45369,7 @@ function toCanonicalToolName$5(continueName) {
44482
45369
  * `Tool(pattern)`.
44483
45370
  */
44484
45371
  function buildContinuePermissionEntry(toolName, pattern) {
44485
- return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
45372
+ return pattern === CATCH_ALL_PATTERN$6 ? toolName : `${toolName}(${pattern})`;
44486
45373
  }
44487
45374
  /**
44488
45375
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -44496,7 +45383,7 @@ function parseContinuePermissionEntry(entry) {
44496
45383
  if (toolName === "") return;
44497
45384
  return {
44498
45385
  toolName,
44499
- pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
45386
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$6 : pattern
44500
45387
  };
44501
45388
  }
44502
45389
  /**
@@ -44670,7 +45557,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
44670
45557
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
44671
45558
  continue;
44672
45559
  }
44673
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
45560
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$6) {
44674
45561
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
44675
45562
  continue;
44676
45563
  }
@@ -44695,7 +45582,7 @@ function convertContinuePermissionsToRulesync(lists) {
44695
45582
  const parsedEntry = parseContinuePermissionEntry(entry);
44696
45583
  if (parsedEntry === void 0) continue;
44697
45584
  if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
44698
- const category = toCanonicalToolName$5(parsedEntry.toolName);
45585
+ const category = toCanonicalToolName$6(parsedEntry.toolName);
44699
45586
  permission[category] ??= {};
44700
45587
  permission[category][parsedEntry.pattern] ??= action;
44701
45588
  }
@@ -45019,14 +45906,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
45019
45906
  };
45020
45907
  //#endregion
45021
45908
  //#region src/features/permissions/crush-permissions.ts
45022
- const CATCH_ALL_PATTERN$4 = "*";
45909
+ const CATCH_ALL_PATTERN$5 = "*";
45023
45910
  /**
45024
45911
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
45025
45912
  * argument patterns at all, so it is the only pattern a rule can carry and
45026
45913
  * still be written.
45027
45914
  */
45028
45915
  function normalizeCatchAllPattern(pattern) {
45029
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$4 : pattern;
45916
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
45030
45917
  }
45031
45918
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
45032
45919
  bash: "bash",
@@ -45038,14 +45925,14 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
45038
45925
  webfetch: "fetch"
45039
45926
  };
45040
45927
  const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
45041
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
45928
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
45042
45929
  const MCP_CRUSH_PREFIX = "mcp_";
45043
45930
  function toCrushToolName(canonical) {
45044
45931
  if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
45045
- if (canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
45932
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
45046
45933
  return canonical;
45047
45934
  }
45048
- function toCanonicalToolName$4(crushName) {
45935
+ function toCanonicalToolName$5(crushName) {
45049
45936
  return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
45050
45937
  }
45051
45938
  function isMcpToolName(crushName) {
@@ -45225,7 +46112,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
45225
46112
  for (const [rawPattern, action] of Object.entries(rules)) {
45226
46113
  if (isPrototypePollutionKey(rawPattern)) continue;
45227
46114
  const pattern = normalizeCatchAllPattern(rawPattern);
45228
- if (pattern === CATCH_ALL_PATTERN$4) {
46115
+ if (pattern === CATCH_ALL_PATTERN$5) {
45229
46116
  catchAllAction = action;
45230
46117
  continue;
45231
46118
  }
@@ -45298,11 +46185,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
45298
46185
  };
45299
46186
  for (const entry of disabled) {
45300
46187
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
45301
- bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] = "deny";
46188
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] = "deny";
45302
46189
  }
45303
46190
  for (const entry of allowed) {
45304
46191
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
45305
- bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] ??= "allow";
46192
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] ??= "allow";
45306
46193
  }
45307
46194
  return { permission };
45308
46195
  }
@@ -45349,13 +46236,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
45349
46236
  WebFetch: "webfetch",
45350
46237
  Mcp: "mcp"
45351
46238
  };
45352
- const MCP_CANONICAL_PREFIX$3 = "mcp__";
46239
+ const MCP_CANONICAL_PREFIX$4 = "mcp__";
45353
46240
  /**
45354
46241
  * Returns true if the canonical category is the per-tool MCP form
45355
46242
  * `mcp__<server>__<tool>`.
45356
46243
  */
45357
46244
  function isMcpScopedCategory(canonical) {
45358
- return canonical.startsWith(MCP_CANONICAL_PREFIX$3) && canonical.length > 5;
46245
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5;
45359
46246
  }
45360
46247
  function toCursorType(canonical) {
45361
46248
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -45389,7 +46276,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
45389
46276
  if (match) {
45390
46277
  const server = match[1] ?? "*";
45391
46278
  const tool = match[2] ?? "*";
45392
- return `${MCP_CANONICAL_PREFIX$3}${server}__${tool}`;
46279
+ return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
45393
46280
  }
45394
46281
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
45395
46282
  }
@@ -45682,7 +46569,7 @@ function convertCursorToRulesyncPermissions(params) {
45682
46569
  const { type, pattern } = parseCursorPermissionEntry(entry);
45683
46570
  const canonical = toCanonicalCategory$1(type, pattern);
45684
46571
  if (!permission[canonical]) permission[canonical] = {};
45685
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$3) ? "*" : pattern;
46572
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$4) ? "*" : pattern;
45686
46573
  permission[canonical][canonicalPattern] = action;
45687
46574
  }
45688
46575
  };
@@ -47201,7 +48088,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
47201
48088
  //#region src/features/permissions/goose-permissions.ts
47202
48089
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
47203
48090
  const GOOSE_USER_KEY = "user";
47204
- const CATCH_ALL_PATTERN$3 = "*";
48091
+ const CATCH_ALL_PATTERN$4 = "*";
47205
48092
  const RULESYNC_TO_GOOSE_TOOL_NAME = {
47206
48093
  bash: "developer__shell",
47207
48094
  edit: "developer__text_editor",
@@ -47363,7 +48250,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
47363
48250
  for (const [category, rules] of orderedEntries) {
47364
48251
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
47365
48252
  for (const [pattern, action] of Object.entries(rules)) {
47366
- if (pattern !== CATCH_ALL_PATTERN$3) {
48253
+ if (pattern !== CATCH_ALL_PATTERN$4) {
47367
48254
  logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
47368
48255
  continue;
47369
48256
  }
@@ -47392,7 +48279,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
47392
48279
  for (const toolName of toolNames) {
47393
48280
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
47394
48281
  permission[category] ??= {};
47395
- permission[category][CATCH_ALL_PATTERN$3] = action;
48282
+ permission[category][CATCH_ALL_PATTERN$4] = action;
47396
48283
  }
47397
48284
  }
47398
48285
  return { permission };
@@ -47403,8 +48290,8 @@ const GROKCLI_UI_KEY = "ui";
47403
48290
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
47404
48291
  const GROKCLI_PERMISSION_KEY = "permission";
47405
48292
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
47406
- const CATCH_ALL_PATTERN$2 = "*";
47407
- const MCP_CANONICAL_PREFIX$2 = "mcp__";
48293
+ const CATCH_ALL_PATTERN$3 = "*";
48294
+ const MCP_CANONICAL_PREFIX$3 = "mcp__";
47408
48295
  const CATEGORY_TO_GROK_TOOL = {
47409
48296
  bash: "Bash",
47410
48297
  read: "Read",
@@ -47445,14 +48332,14 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
47445
48332
  * concrete pattern emits `Tool(pattern)`.
47446
48333
  */
47447
48334
  function buildGrokEntry(category, pattern) {
47448
- if (category.startsWith(MCP_CANONICAL_PREFIX$2)) {
48335
+ if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
47449
48336
  const remainder = category.slice(5);
47450
48337
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
47451
48338
  }
47452
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
48339
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
47453
48340
  const tool = CATEGORY_TO_GROK_TOOL[category];
47454
48341
  if (tool === void 0) return null;
47455
- return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
48342
+ return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? tool : `${tool}(${pattern})`;
47456
48343
  }
47457
48344
  /**
47458
48345
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -47472,17 +48359,17 @@ function parseGrokEntry(entry) {
47472
48359
  inner = trimmed.slice(parenIndex + 1, -1).trim();
47473
48360
  }
47474
48361
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
47475
- category: `${MCP_CANONICAL_PREFIX$2}${inner}`,
47476
- pattern: CATCH_ALL_PATTERN$2
48362
+ category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
48363
+ pattern: CATCH_ALL_PATTERN$3
47477
48364
  } : {
47478
48365
  category: "mcp",
47479
- pattern: CATCH_ALL_PATTERN$2
48366
+ pattern: CATCH_ALL_PATTERN$3
47480
48367
  };
47481
48368
  const category = GROK_TOOL_TO_CATEGORY[tool];
47482
48369
  if (category === void 0) return null;
47483
48370
  return {
47484
48371
  category,
47485
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
48372
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$3
47486
48373
  };
47487
48374
  }
47488
48375
  /**
@@ -47503,15 +48390,15 @@ function parseGrokRule(rule) {
47503
48390
  if (typeof tool !== "string") return null;
47504
48391
  if (pattern !== void 0 && typeof pattern !== "string") return null;
47505
48392
  const trimmedPattern = pattern?.trim() ?? "";
47506
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
48393
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$3;
47507
48394
  const lowerTool = tool.trim().toLowerCase();
47508
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
48395
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$3 ? {
47509
48396
  category: "mcp",
47510
- pattern: CATCH_ALL_PATTERN$2,
48397
+ pattern: CATCH_ALL_PATTERN$3,
47511
48398
  action
47512
48399
  } : {
47513
- category: `${MCP_CANONICAL_PREFIX$2}${resolvedPattern}`,
47514
- pattern: CATCH_ALL_PATTERN$2,
48400
+ category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
48401
+ pattern: CATCH_ALL_PATTERN$3,
47515
48402
  action
47516
48403
  };
47517
48404
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -47659,7 +48546,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
47659
48546
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
47660
48547
  }
47661
48548
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
47662
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
48549
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$3]: legacyModeAction(parsed) } } };
47663
48550
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
47664
48551
  }
47665
48552
  validate() {
@@ -48395,7 +49282,7 @@ function isSharedKiloCategory(key) {
48395
49282
  */
48396
49283
  function parseKiloJsoncStrict(content, filePath) {
48397
49284
  const errors = [];
48398
- const parsed = parse(content, errors, { allowTrailingComma: true });
49285
+ const parsed = parse$1(content, errors, { allowTrailingComma: true });
48399
49286
  const first = errors[0];
48400
49287
  if (first) throw new Error(`Failed to parse Kilo Code config at ${filePath}: ${printParseErrorCode(first.error)} at offset ${first.offset}`);
48401
49288
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
@@ -48439,7 +49326,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
48439
49326
  json;
48440
49327
  constructor(params) {
48441
49328
  super(params);
48442
- const parsed = parse(this.fileContent || "{}");
49329
+ const parsed = parse$1(this.fileContent || "{}");
48443
49330
  if (params.validate !== false) this.json = KiloPermissionsConfigSchema.parse(parsed);
48444
49331
  else {
48445
49332
  const result = KiloPermissionsConfigSchema.safeParse(parsed);
@@ -48579,7 +49466,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
48579
49466
  }
48580
49467
  validate() {
48581
49468
  try {
48582
- const json = parse(this.fileContent || "{}");
49469
+ const json = parse$1(this.fileContent || "{}");
48583
49470
  const result = KiloPermissionsConfigSchema.safeParse(json);
48584
49471
  if (!result.success) return {
48585
49472
  success: false,
@@ -49290,7 +50177,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
49290
50177
  json;
49291
50178
  constructor(params) {
49292
50179
  super(params);
49293
- this.json = OpencodePermissionsConfigSchema.parse(parse(this.fileContent || "{}"));
50180
+ this.json = OpencodePermissionsConfigSchema.parse(parse$1(this.fileContent || "{}"));
49294
50181
  }
49295
50182
  getJson() {
49296
50183
  return this.json;
@@ -49318,7 +50205,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
49318
50205
  fileContent = await readFileContentOrNull(jsonPath);
49319
50206
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
49320
50207
  }
49321
- const parsed = parse(fileContent ?? "{}");
50208
+ const parsed = parse$1(fileContent ?? "{}");
49322
50209
  const record = isRecord$1(parsed) ? parsed : {};
49323
50210
  const nextJson = {
49324
50211
  ...record,
@@ -49539,6 +50426,567 @@ var PiPermissions = class PiPermissions extends ToolPermissions {
49539
50426
  }
49540
50427
  };
49541
50428
  //#endregion
50429
+ //#region src/features/permissions/pool-permissions.ts
50430
+ const POOL_ALLOW_KEY = "allow";
50431
+ const POOL_DENY_KEY = "deny";
50432
+ const POOL_DISABLED_KEY = "disabled";
50433
+ const POOL_PATH_KEY = "path";
50434
+ const POOL_WRITE_KEY = "write";
50435
+ const CATCH_ALL_PATTERN$2 = "*";
50436
+ const PATHS_CATCH_ALL_PATTERN = "**";
50437
+ const CANONICAL_TO_POOL_TOOL_NAMES = {
50438
+ bash: "shell",
50439
+ webfetch: "web_fetch",
50440
+ websearch: "web_search"
50441
+ };
50442
+ const POOL_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_TOOL_NAMES).map(([k, v]) => [v, k]));
50443
+ const READ_CATEGORY = "read";
50444
+ const WRITE_CATEGORIES = ["edit", "write"];
50445
+ const FILE_CATEGORIES = [READ_CATEGORY, ...WRITE_CATEGORIES];
50446
+ const MCP_CANONICAL_PREFIX$2 = "mcp__";
50447
+ /**
50448
+ * Single spelling of the settings.yaml codec/policy, matching the
50449
+ * `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
50450
+ * unparseable root rather than replacing the user's primary Pool settings with
50451
+ * generated output.
50452
+ */
50453
+ function parsePoolSettings$1(fileContent, filePath) {
50454
+ return parseSharedConfig({
50455
+ format: "yaml",
50456
+ fileContent,
50457
+ filePath,
50458
+ invalidRootPolicy: "error"
50459
+ });
50460
+ }
50461
+ function toPoolToolName(canonical) {
50462
+ return lookupOwn({
50463
+ record: CANONICAL_TO_POOL_TOOL_NAMES,
50464
+ key: canonical
50465
+ }) ?? canonical;
50466
+ }
50467
+ function toCanonicalToolName$4(poolName) {
50468
+ return lookupOwn({
50469
+ record: POOL_TO_CANONICAL_TOOL_NAMES,
50470
+ key: poolName
50471
+ }) ?? poolName;
50472
+ }
50473
+ function isMcpCategory(category) {
50474
+ return category.startsWith(MCP_CANONICAL_PREFIX$2) && category.length > 5;
50475
+ }
50476
+ function isFileCategory(category) {
50477
+ return FILE_CATEGORIES.includes(category);
50478
+ }
50479
+ /**
50480
+ * Pool's tool rules know one wildcard, `*`, which matches anything including
50481
+ * `/` — the same set a canonical `*` names — so a `**` run is collapsed to
50482
+ * keep the file in Pool's own spelling. `?`, `[...]` and `{a,b}` are literal
50483
+ * characters to Pool, so a pattern that relies on them cannot be written.
50484
+ */
50485
+ function toPoolToolPattern(pattern) {
50486
+ if (/[?[{]/.test(pattern)) return void 0;
50487
+ return pattern.replaceAll(/\*{2,}/g, "*");
50488
+ }
50489
+ /**
50490
+ * Pool's path globs know `*` and `**`, so a bare `*` run is written as `**`
50491
+ * to keep it matching anything. `?`, `[...]` and `{a,b}` are literal
50492
+ * characters to Pool here as well, so a pattern that relies on them cannot be
50493
+ * written.
50494
+ */
50495
+ function toPoolPathPattern(pattern) {
50496
+ if (/[?[{]/.test(pattern)) return void 0;
50497
+ return /^\*+$/.test(pattern) ? PATHS_CATCH_ALL_PATTERN : pattern;
50498
+ }
50499
+ function isHomeOrAbsolutePath(pattern) {
50500
+ return isAbsolute(pattern) || pattern === "~" || pattern.startsWith("~/");
50501
+ }
50502
+ function rulesOf(permission, category) {
50503
+ return lookupOwn({
50504
+ record: permission,
50505
+ key: category
50506
+ }) ?? {};
50507
+ }
50508
+ function patternsWithAction(rules, actions) {
50509
+ return Object.entries(rules).filter(([pattern, action]) => !isPrototypePollutionKey(pattern) && actions.includes(action)).map(([pattern]) => pattern);
50510
+ }
50511
+ function asRestrictions(patterns) {
50512
+ return uniq(patterns).map((pattern) => ({
50513
+ pattern,
50514
+ fromAllToolsCategory: false
50515
+ }));
50516
+ }
50517
+ function listPatterns(patterns) {
50518
+ return patterns.map((pattern) => quoteValueForWarning(pattern)).join(", ");
50519
+ }
50520
+ /**
50521
+ * Permissions adapter for Pool (Poolside's coding agent CLI).
50522
+ *
50523
+ * Pool keeps its permission rules in the same YAML settings file as its MCP
50524
+ * servers — `.poolside/settings.yaml` (project) / `~/.config/poolside/settings.yaml`
50525
+ * (global) — under two top-level keys:
50526
+ * - `tools.<name>`: `allow` and `deny` pattern lists matched against the
50527
+ * tool's argument (the command for `shell`, the URL for `web_fetch`, ...),
50528
+ * plus `disabled: true` to remove the tool. A denied pattern wins over an
50529
+ * allowed one; a call that matches neither prompts. Only `*` is a wildcard.
50530
+ * - `paths`: `allow` entries (`{ path }`, read-only unless `write: true`)
50531
+ * and `deny` entries (`{ path }`) with `*`/`**` globs; deny wins.
50532
+ *
50533
+ * Mapping (rulesync canonical -> Pool):
50534
+ * - `bash` -> `tools.shell`, `webfetch` -> `tools.web_fetch`,
50535
+ * `websearch` -> `tools.web_search`; any other non-file category passes
50536
+ * through as `tools.<category>`. `allow` -> `allow`, `deny` -> `deny`,
50537
+ * `ask` -> nothing (prompting is Pool's default) — but an `allow` that
50538
+ * overlaps an `ask` of any category landing on the same Pool tool, or a
50539
+ * deny Pool cannot spell, is withheld, since an allowed pattern would
50540
+ * silence the prompt the config asks for.
50541
+ * - `read` / `edit` / `write` -> `paths`: a `read` allow is a read-only
50542
+ * entry, an `edit` or `write` allow adds `write: true`, a `read` deny is a
50543
+ * deny entry. Pool cannot deny writes without denying reads, so an
50544
+ * `edit`/`write` deny that is not also a `read` deny only withholds the
50545
+ * write flag of the entries it overlaps (Pool prompts for those writes).
50546
+ * - `mcp__<server>__<tool>` is skipped: Pool approves MCP tools per server
50547
+ * under `mcp_servers.<server>.allow`, authored as `poolAllow` in
50548
+ * `.rulesync/mcp.json`. The all-tools `*` category is mirrored onto
50549
+ * `bash` by `honorAllToolsOnBash` and otherwise skipped.
50550
+ *
50551
+ * Project settings take project-relative paths and the user settings absolute
50552
+ * or `~` paths, so a path of the other kind is skipped with a warning at each
50553
+ * scope. The managed tools' `allow`/`deny` lists and, whenever the canonical
50554
+ * config carries a file category, the `paths` lists are rebuilt; every other
50555
+ * key (`disabled`, unmanaged tools, `mcp_servers`, `pool`, `sandbox`, ...) is
50556
+ * kept, and the file is never deleted.
50557
+ *
50558
+ * @see https://docs.poolside.ai/settings-file-reference
50559
+ */
50560
+ var PoolPermissions = class PoolPermissions extends ToolPermissions {
50561
+ settings;
50562
+ constructor(params) {
50563
+ super({
50564
+ ...params,
50565
+ fileContent: params.fileContent ?? ""
50566
+ });
50567
+ this.settings = parsePoolSettings$1(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
50568
+ }
50569
+ getSettings() {
50570
+ return this.settings;
50571
+ }
50572
+ isDeletable() {
50573
+ return false;
50574
+ }
50575
+ /**
50576
+ * settings.yaml is Pool's file: rulesync merges into it when it exists but
50577
+ * does not create one that holds nothing of its own — an absent file and an
50578
+ * absent rule both mean "prompt".
50579
+ */
50580
+ shouldSkipCreationWhenPayloadEmpty() {
50581
+ return true;
50582
+ }
50583
+ static getSettablePaths({ global = false } = {}) {
50584
+ return {
50585
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
50586
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
50587
+ };
50588
+ }
50589
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
50590
+ const paths = this.getSettablePaths({ global });
50591
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
50592
+ return new PoolPermissions({
50593
+ outputRoot,
50594
+ relativeDirPath: paths.relativeDirPath,
50595
+ relativeFilePath: paths.relativeFilePath,
50596
+ fileContent,
50597
+ validate,
50598
+ global
50599
+ });
50600
+ }
50601
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
50602
+ const paths = this.getSettablePaths({ global });
50603
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
50604
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
50605
+ const existing = parsePoolSettings$1(existingContent, filePath);
50606
+ const patch = buildPoolPermissionsPatch({
50607
+ config: rulesyncPermissions.getJson(),
50608
+ existing,
50609
+ global,
50610
+ logger
50611
+ });
50612
+ return new PoolPermissions({
50613
+ outputRoot,
50614
+ relativeDirPath: paths.relativeDirPath,
50615
+ relativeFilePath: paths.relativeFilePath,
50616
+ fileContent: applySharedConfigPatch({
50617
+ fileKey: sharedConfigFileKey(paths),
50618
+ feature: "permissions",
50619
+ existingContent,
50620
+ patch,
50621
+ filePath,
50622
+ logger
50623
+ }),
50624
+ validate: true,
50625
+ global
50626
+ });
50627
+ }
50628
+ toRulesyncPermissions() {
50629
+ const rulesyncConfig = convertPoolToRulesync({
50630
+ tools: isRecord$1(this.settings["tools"]) ? this.settings[POOL_TOOLS_KEY] : {},
50631
+ paths: isRecord$1(this.settings["paths"]) ? this.settings[POOL_PATHS_KEY] : {}
50632
+ });
50633
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
50634
+ }
50635
+ validate() {
50636
+ return {
50637
+ success: true,
50638
+ error: null
50639
+ };
50640
+ }
50641
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50642
+ return new PoolPermissions({
50643
+ outputRoot,
50644
+ relativeDirPath,
50645
+ relativeFilePath,
50646
+ fileContent: "",
50647
+ validate: false,
50648
+ global
50649
+ });
50650
+ }
50651
+ };
50652
+ /**
50653
+ * The deep-merge patch that rewrites the managed `tools.<name>` lists and,
50654
+ * when the canonical config carries a file category, the `paths` lists. An
50655
+ * empty list retracts its key (`undefined` deletes under deep-merge), and a
50656
+ * block is only touched when there is something to write into it or it
50657
+ * already exists, so a config without rules does not grow an empty
50658
+ * `tools: {}` / `paths: {}`.
50659
+ */
50660
+ function buildPoolPermissionsPatch({ config, existing, global, logger }) {
50661
+ const permission = honorAllToolsOnBash(config.permission);
50662
+ const existingTools = isRecord$1(existing["tools"]) ? existing[POOL_TOOLS_KEY] : void 0;
50663
+ const existingPaths = isRecord$1(existing["paths"]) ? existing[POOL_PATHS_KEY] : void 0;
50664
+ const patch = {};
50665
+ const toolsPatch = {};
50666
+ for (const [toolName, lists] of convertRulesyncToPoolTools({
50667
+ permission,
50668
+ logger
50669
+ })) {
50670
+ const existingTool = existingTools === void 0 ? void 0 : lookupOwn({
50671
+ record: existingTools,
50672
+ key: toolName
50673
+ });
50674
+ if (lists.allow.length === 0 && lists.deny.length === 0 && existingTool === void 0) continue;
50675
+ toolsPatch[toolName] = {
50676
+ [POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
50677
+ [POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
50678
+ };
50679
+ }
50680
+ if (Object.keys(toolsPatch).length > 0) patch[POOL_TOOLS_KEY] = toolsPatch;
50681
+ if (FILE_CATEGORIES.some((category) => lookupOwn({
50682
+ record: permission,
50683
+ key: category
50684
+ }))) {
50685
+ const lists = convertRulesyncToPoolPaths({
50686
+ permission,
50687
+ global,
50688
+ logger
50689
+ });
50690
+ if (lists.allow.length > 0 || lists.deny.length > 0 || existingPaths !== void 0) patch[POOL_PATHS_KEY] = {
50691
+ [POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
50692
+ [POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
50693
+ };
50694
+ }
50695
+ return patch;
50696
+ }
50697
+ /**
50698
+ * Convert one non-file category into a Pool tool's `allow`/`deny` lists. A
50699
+ * deny Pool can spell needs no withholding (Pool lets it win over an allow),
50700
+ * but an `ask` has no list of its own — a call Pool does not match prompts —
50701
+ * so an allow that overlaps one is withheld and reported, and the tool keeps
50702
+ * prompting. A deny Pool cannot spell (its pattern uses a glob form other
50703
+ * than `*`) is skipped, and then withholds the allows it overlaps the same
50704
+ * way: written, such an allow would auto-approve the very command the deny
50705
+ * was meant to stop. `restrictions` carries the ask patterns and the skipped
50706
+ * deny patterns of every category that lands on the same Pool tool, because
50707
+ * Pool matches the joined lists as one.
50708
+ */
50709
+ function convertToolCategory({ category, toolName, rules, restrictions, logger }) {
50710
+ const lists = {
50711
+ allow: [],
50712
+ deny: []
50713
+ };
50714
+ const shadowingRestrictions = createShadowingRestrictionsTest(asRestrictions(restrictions));
50715
+ for (const [rawPattern, action] of Object.entries(rules)) {
50716
+ if (isPrototypePollutionKey(rawPattern) || action === "ask") continue;
50717
+ const pattern = toPoolToolPattern(rawPattern);
50718
+ if (pattern === void 0) {
50719
+ logger?.warn(`Pool tool rules treat only "*" as a wildcard, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
50720
+ continue;
50721
+ }
50722
+ if (action === "deny") {
50723
+ lists.deny.push(pattern);
50724
+ continue;
50725
+ }
50726
+ const shadowing = shadowingRestrictions(rawPattern);
50727
+ if (shadowing.length > 0) {
50728
+ logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched call prompts), so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool keeps prompting for "${toolName}".`);
50729
+ continue;
50730
+ }
50731
+ lists.allow.push(pattern);
50732
+ }
50733
+ return {
50734
+ allow: uniq(lists.allow),
50735
+ deny: uniq(lists.deny)
50736
+ };
50737
+ }
50738
+ function unrepresentableDenies(rules) {
50739
+ return patternsWithAction(rules, ["deny"]).filter((pattern) => toPoolToolPattern(pattern) === void 0);
50740
+ }
50741
+ /**
50742
+ * Group the non-file categories by the Pool tool they land on, reporting the
50743
+ * ones Pool has no place for. Two categories can resolve to the same tool
50744
+ * (`bash` and a pass-through `shell`).
50745
+ */
50746
+ function groupCategoriesByPoolTool({ permission, logger }) {
50747
+ const byTool = /* @__PURE__ */ new Map();
50748
+ for (const [category, rules] of Object.entries(permission)) {
50749
+ if (isPrototypePollutionKey(category) || isFileCategory(category)) continue;
50750
+ if (category === "*") {
50751
+ for (const [rawPattern, action] of Object.entries(rules)) {
50752
+ if (isPrototypePollutionKey(rawPattern)) continue;
50753
+ logger?.warn(`Pool has no rule that spans every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
50754
+ }
50755
+ continue;
50756
+ }
50757
+ if (isMcpCategory(category)) {
50758
+ logger?.warn(`Pool approves MCP tools per server under "mcp_servers.<server>.allow", so the rules for "${category}" were skipped; author them as "poolAllow" on the server in .rulesync/mcp.json instead.`);
50759
+ continue;
50760
+ }
50761
+ const toolName = toPoolToolName(category);
50762
+ byTool.set(toolName, [...byTool.get(toolName) ?? [], [category, rules]]);
50763
+ }
50764
+ return byTool;
50765
+ }
50766
+ /**
50767
+ * Convert the non-file categories into Pool tool lists, keyed by Pool tool
50768
+ * name. The categories that land on the same tool are converted against the
50769
+ * union of their `ask` patterns and their lists are joined, which is safe
50770
+ * because a deny wins over an allow in Pool whichever category wrote it.
50771
+ */
50772
+ function convertRulesyncToPoolTools({ permission, logger }) {
50773
+ const byTool = /* @__PURE__ */ new Map();
50774
+ for (const [toolName, categories] of groupCategoriesByPoolTool({
50775
+ permission,
50776
+ logger
50777
+ })) {
50778
+ const restrictions = categories.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...unrepresentableDenies(rules)]);
50779
+ const lists = {
50780
+ allow: [],
50781
+ deny: []
50782
+ };
50783
+ for (const [category, rules] of categories) {
50784
+ const converted = convertToolCategory({
50785
+ category,
50786
+ toolName,
50787
+ rules,
50788
+ restrictions,
50789
+ logger
50790
+ });
50791
+ lists.allow.push(...converted.allow);
50792
+ lists.deny.push(...converted.deny);
50793
+ }
50794
+ byTool.set(toolName, {
50795
+ allow: uniq(lists.allow),
50796
+ deny: uniq(lists.deny)
50797
+ });
50798
+ }
50799
+ return byTool;
50800
+ }
50801
+ /**
50802
+ * Convert the file categories into Pool's `paths` lists.
50803
+ *
50804
+ * An allow entry grants reading, and `write: true` grants writing on top, so
50805
+ * a `read` allow is withheld by an overlapping `read` ask and a write allow by
50806
+ * an overlapping `edit`/`write` ask — and by a `read` ask, since the write
50807
+ * entry would read too. A `read` deny lands in `paths.deny`, which Pool lets
50808
+ * win over any allow, and covers an `edit`/`write` deny of the same pattern,
50809
+ * so neither withholds anything; an `edit`/`write` deny without a `read` deny
50810
+ * cannot be written (Pool's deny blocks reads as well), so it is reported and
50811
+ * only withholds the write flags it overlaps. A deny Pool cannot spell is
50812
+ * skipped the same way and withholds the allows it overlaps, so it is honored
50813
+ * by prompting rather than dropped.
50814
+ */
50815
+ function convertRulesyncToPoolPaths({ permission, global, logger }) {
50816
+ const readRules = rulesOf(permission, READ_CATEGORY);
50817
+ const writeRulesByCategory = WRITE_CATEGORIES.map((category) => [category, rulesOf(permission, category)]);
50818
+ const readAsks = patternsWithAction(readRules, ["ask"]);
50819
+ const fitsScope = ({ category, action, pattern }) => {
50820
+ if (isHomeOrAbsolutePath(pattern) === global) return true;
50821
+ logger?.warn(global ? `Pool's user settings take absolute or "~" paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ${join(POOL_DIR, POOL_SETTINGS_FILE_NAME)} (without --global) instead.` : `Pool's project settings take project-relative paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ~/${join(POOL_GLOBAL_DIR, POOL_SETTINGS_FILE_NAME)} (via --global) instead.`);
50822
+ return false;
50823
+ };
50824
+ const deny = [];
50825
+ const readRestrictions = [...readAsks];
50826
+ for (const pattern of patternsWithAction(readRules, ["deny"])) {
50827
+ const path = toPoolPathPattern(pattern);
50828
+ if (path === void 0) {
50829
+ warnSkippedPathPattern({
50830
+ category: READ_CATEGORY,
50831
+ action: "deny",
50832
+ pattern,
50833
+ logger
50834
+ });
50835
+ readRestrictions.push(pattern);
50836
+ continue;
50837
+ }
50838
+ if (!fitsScope({
50839
+ category: READ_CATEGORY,
50840
+ action: "deny",
50841
+ pattern
50842
+ })) continue;
50843
+ if (!deny.some((entry) => entry[POOL_PATH_KEY] === path)) deny.push({ [POOL_PATH_KEY]: path });
50844
+ }
50845
+ const isDeniedForRead = (pattern) => {
50846
+ const path = toPoolPathPattern(pattern);
50847
+ return path !== void 0 && deny.some((entry) => entry[POOL_PATH_KEY] === path);
50848
+ };
50849
+ const writeRestrictions = writeRulesByCategory.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...patternsWithAction(rules, ["deny"]).filter((pattern) => !isDeniedForRead(pattern))]);
50850
+ reportUnwrittenWriteDenies({
50851
+ writeRulesByCategory,
50852
+ isDeniedForRead,
50853
+ logger
50854
+ });
50855
+ const shadowingReadRestrictions = createShadowingRestrictionsTest(asRestrictions(readRestrictions));
50856
+ const shadowingWriteRestrictions = createShadowingRestrictionsTest(asRestrictions([...readRestrictions, ...writeRestrictions]));
50857
+ const allowByPath = /* @__PURE__ */ new Map();
50858
+ for (const pattern of patternsWithAction(readRules, ["allow"])) {
50859
+ if (!fitsScope({
50860
+ category: READ_CATEGORY,
50861
+ action: "allow",
50862
+ pattern
50863
+ })) continue;
50864
+ const path = toPoolPathPattern(pattern);
50865
+ if (path === void 0) {
50866
+ warnSkippedPathPattern({
50867
+ category: READ_CATEGORY,
50868
+ action: "allow",
50869
+ pattern,
50870
+ logger
50871
+ });
50872
+ continue;
50873
+ }
50874
+ const shadowing = shadowingReadRestrictions(pattern);
50875
+ if (shadowing.length > 0) {
50876
+ logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched path prompts), so the "allow" rule for "read" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}.`);
50877
+ continue;
50878
+ }
50879
+ allowByPath.set(path, { [POOL_PATH_KEY]: path });
50880
+ }
50881
+ for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["allow"])) {
50882
+ if (!fitsScope({
50883
+ category,
50884
+ action: "allow",
50885
+ pattern
50886
+ })) continue;
50887
+ const path = toPoolPathPattern(pattern);
50888
+ if (path === void 0) {
50889
+ warnSkippedPathPattern({
50890
+ category,
50891
+ action: "allow",
50892
+ pattern,
50893
+ logger
50894
+ });
50895
+ continue;
50896
+ }
50897
+ const shadowing = shadowingWriteRestrictions(pattern);
50898
+ if (shadowing.length > 0) {
50899
+ logger?.warn(`A Pool path allowed with "write: true" can be read and written freely, so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool prompts for those writes.`);
50900
+ continue;
50901
+ }
50902
+ allowByPath.set(path, {
50903
+ [POOL_PATH_KEY]: path,
50904
+ [POOL_WRITE_KEY]: true
50905
+ });
50906
+ }
50907
+ return {
50908
+ allow: [...allowByPath.values()],
50909
+ deny
50910
+ };
50911
+ }
50912
+ function warnSkippedPathPattern({ category, action, pattern, logger }) {
50913
+ logger?.warn(`Pool path rules treat only "*" and "**" as wildcards, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
50914
+ }
50915
+ function reportUnwrittenWriteDenies({ writeRulesByCategory, isDeniedForRead, logger }) {
50916
+ for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["deny"])) {
50917
+ if (isDeniedForRead(pattern)) continue;
50918
+ if (toPoolPathPattern(pattern) === void 0) {
50919
+ warnSkippedPathPattern({
50920
+ category,
50921
+ action: "deny",
50922
+ pattern,
50923
+ logger
50924
+ });
50925
+ continue;
50926
+ }
50927
+ logger?.warn(`Pool cannot deny writes without denying reads, so the "deny" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was not written and Pool prompts for those writes instead; deny the pattern under "read" as well to block it entirely.`);
50928
+ }
50929
+ }
50930
+ function isUsablePattern(pattern) {
50931
+ return typeof pattern === "string" && pattern !== "" && !isPrototypePollutionKey(pattern);
50932
+ }
50933
+ function pathOfEntry(entry) {
50934
+ return isRecord$1(entry) && isUsablePattern(entry[POOL_PATH_KEY]) ? entry[POOL_PATH_KEY] : void 0;
50935
+ }
50936
+ /**
50937
+ * Convert one `tools.<name>` block into its canonical bucket. A disabled tool
50938
+ * never runs, so it reads as a catch-all deny and its own lists are ignored;
50939
+ * a denied pattern wins over an allowed one, as it does in Pool.
50940
+ */
50941
+ function importPoolTool({ toolConfig, bucket }) {
50942
+ if (toolConfig[POOL_DISABLED_KEY] === true) {
50943
+ bucket[CATCH_ALL_PATTERN$2] = "deny";
50944
+ return;
50945
+ }
50946
+ const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
50947
+ const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
50948
+ for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
50949
+ for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
50950
+ }
50951
+ /**
50952
+ * Convert Pool's `tools` and `paths` blocks back into a rulesync config.
50953
+ * `mcp_servers` approvals are the mcp feature's (`poolAllow`) and are not read.
50954
+ */
50955
+ function convertPoolToRulesync({ tools, paths }) {
50956
+ const permission = {};
50957
+ const bucketFor = (category) => {
50958
+ const own = lookupOwn({
50959
+ record: permission,
50960
+ key: category
50961
+ });
50962
+ if (own !== void 0) return own;
50963
+ const created = {};
50964
+ permission[category] = created;
50965
+ return created;
50966
+ };
50967
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
50968
+ if (isPrototypePollutionKey(toolName) || !isRecord$1(toolConfig)) continue;
50969
+ importPoolTool({
50970
+ toolConfig,
50971
+ bucket: bucketFor(toCanonicalToolName$4(toolName))
50972
+ });
50973
+ }
50974
+ const denyEntries = Array.isArray(paths[POOL_DENY_KEY]) ? paths[POOL_DENY_KEY] : [];
50975
+ const allowEntries = Array.isArray(paths[POOL_ALLOW_KEY]) ? paths[POOL_ALLOW_KEY] : [];
50976
+ for (const entry of denyEntries) {
50977
+ const path = pathOfEntry(entry);
50978
+ if (path === void 0) continue;
50979
+ for (const category of FILE_CATEGORIES) bucketFor(category)[path] = "deny";
50980
+ }
50981
+ for (const entry of allowEntries) {
50982
+ const path = pathOfEntry(entry);
50983
+ if (path === void 0) continue;
50984
+ bucketFor(READ_CATEGORY)[path] ??= "allow";
50985
+ if (isRecord$1(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
50986
+ }
50987
+ return { permission };
50988
+ }
50989
+ //#endregion
49542
50990
  //#region src/features/permissions/qwencode-permissions.ts
49543
50991
  /**
49544
50992
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -55641,6 +57089,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
55641
57089
  supportsImport: true
55642
57090
  }
55643
57091
  }],
57092
+ ["pool", {
57093
+ class: PoolPermissions,
57094
+ meta: {
57095
+ supportsProject: true,
57096
+ supportsGlobal: true,
57097
+ supportsImport: true
57098
+ }
57099
+ }],
55644
57100
  ["qwencode", {
55645
57101
  class: QwencodePermissions,
55646
57102
  meta: {
@@ -56304,8 +57760,19 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
56304
57760
  const changedPaths = [];
56305
57761
  for (const aiDir of aiDirs) {
56306
57762
  const dirPath = aiDir.getDirPath();
56307
- let dirHasChanges = false;
56308
57763
  const mainFile = aiDir.getMainFile();
57764
+ const otherFiles = aiDir.getOtherFiles();
57765
+ const writtenPaths = [
57766
+ dirPath,
57767
+ ...mainFile ? [join(dirPath, mainFile.name)] : [],
57768
+ ...otherFiles.map((file) => join(dirPath, file.relativeFilePathToDirPath))
57769
+ ];
57770
+ if (await refusesAnyWriteOutsideRoot({
57771
+ logger: this.logger,
57772
+ rootPath: aiDir.getOutputRoot(),
57773
+ targetPaths: writtenPaths
57774
+ })) continue;
57775
+ let dirHasChanges = false;
56309
57776
  let mainFileContent;
56310
57777
  if (mainFile) {
56311
57778
  const mainFilePath = join(dirPath, mainFile.name);
@@ -56317,7 +57784,6 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
56317
57784
  existing: existingContent
56318
57785
  })) dirHasChanges = true;
56319
57786
  }
56320
- const otherFiles = aiDir.getOtherFiles();
56321
57787
  for (const file of otherFiles) {
56322
57788
  if (dirHasChanges) break;
56323
57789
  const filePath = join(dirPath, file.relativeFilePathToDirPath);
@@ -56762,7 +58228,7 @@ const AmpSkillFrontmatterSchema = z.looseObject({
56762
58228
  * Amp natively reads Agent Skills following the open standard: a directory with a
56763
58229
  * `SKILL.md` carrying `name`/`description` frontmatter, scoped to the project
56764
58230
  * (`.agents/skills/`) and user-wide (`~/.config/agents/skills/`).
56765
- * @see https://ampcode.com/manual
58231
+ * @see https://ampcode.com/docs/customize/skills
56766
58232
  */
56767
58233
  var AmpSkill = class AmpSkill extends ToolSkill {
56768
58234
  constructor({ outputRoot = process.cwd(), relativeDirPath = AMP_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -61731,37 +63197,6 @@ var PiSkill = class PiSkill extends ToolSkill {
61731
63197
  }
61732
63198
  };
61733
63199
  //#endregion
61734
- //#region src/constants/pool-paths.ts
61735
- /**
61736
- * Pool (Poolside's coding agent CLI) configuration-layout conventions.
61737
- *
61738
- * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
61739
- * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
61740
- * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
61741
- * path), the project-root `AGENTS.md`, and nested per-directory
61742
- * `AGENTS.md` files from the repository root down through the working
61743
- * directory, deeper files taking precedence. It skips ignored directories
61744
- * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
61745
- *
61746
- * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
61747
- * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
61748
- * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
61749
- * the skill directories of other Agent Skills tools; rulesync writes only the
61750
- * Pool-specific roots and leaves the shared ones to their own targets.
61751
- *
61752
- * @see https://docs.poolside.ai/agent-instructions
61753
- * @see https://docs.poolside.ai/skills
61754
- * @see https://github.com/poolsideai/pool
61755
- */
61756
- /** Project-scoped `.poolside/` directory at the project root. */
61757
- const POOL_DIR = ".poolside";
61758
- /** Global config directory for Pool, relative to the home directory. */
61759
- const POOL_GLOBAL_DIR = join(".config", "poolside");
61760
- /** Project skills root, relative to the project root. */
61761
- const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
61762
- /** Global skills root, relative to the home directory. */
61763
- const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
61764
- //#endregion
61765
63200
  //#region src/features/skills/pool-skill.ts
61766
63201
  const PoolSkillFrontmatterSchema = z.looseObject({
61767
63202
  name: z.string(),
@@ -68467,6 +69902,325 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
68467
69902
  }
68468
69903
  };
68469
69904
  //#endregion
69905
+ //#region src/features/subagents/pool-subagent.ts
69906
+ /** How Pool runs an agent; a custom agent defaults to `in_process`. */
69907
+ const POOL_AGENT_TYPES = [
69908
+ "in_process",
69909
+ "command",
69910
+ "agent_server"
69911
+ ];
69912
+ const DEFAULT_POOL_AGENT_TYPE = "in_process";
69913
+ /**
69914
+ * One entry of `subagents.agents.<name>` in Pool's settings file. `type` is
69915
+ * required by Pool and `description` by every enabled custom agent; the
69916
+ * remaining keys depend on the type (`command`/`args`/`env` for `command`,
69917
+ * `agent_server`/`session_config_options` for `agent_server`). Loose so a key
69918
+ * Pool adds later survives the round-trip.
69919
+ * @see https://docs.poolside.ai/subagents
69920
+ * @see https://docs.poolside.ai/settings-file-reference
69921
+ */
69922
+ const PoolAgentSchema = z.looseObject({
69923
+ type: z.optional(z.enum(POOL_AGENT_TYPES)),
69924
+ description: z.optional(z.string()),
69925
+ instructions: z.optional(z.string()),
69926
+ command: z.optional(z.string()),
69927
+ args: z.optional(z.array(z.string())),
69928
+ env: z.optional(z.record(z.string(), z.string())),
69929
+ agent_server: z.optional(z.string()),
69930
+ session_config_options: z.optional(z.record(z.string(), z.unknown())),
69931
+ inherit_agent_config: z.optional(z.boolean()),
69932
+ disabled: z.optional(z.boolean())
69933
+ });
69934
+ /**
69935
+ * Single spelling of the settings.yaml codec/policy, matching PoolMcp: fail
69936
+ * closed on an unparseable root rather than replacing the user's primary Pool
69937
+ * settings with generated output.
69938
+ */
69939
+ function parsePoolSettings(fileContent, filePath) {
69940
+ return parseSharedConfig({
69941
+ format: "yaml",
69942
+ fileContent,
69943
+ filePath,
69944
+ invalidRootPolicy: "error"
69945
+ });
69946
+ }
69947
+ /**
69948
+ * The file stem an imported agent lands under in `.rulesync/subagents/`. Pool
69949
+ * keys agents by an arbitrary YAML mapping key, so the name is narrowed to a
69950
+ * safe file name before it becomes a path (defense in depth on top of the
69951
+ * central path-traversal guard); the agent keeps its real name in `name`.
69952
+ */
69953
+ function sanitizePoolAgentFileStem(raw) {
69954
+ const stem = raw.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
69955
+ return stem.length > 0 ? stem : "agent";
69956
+ }
69957
+ /**
69958
+ * Read `subagents.agents` out of a parsed settings document. The reserved
69959
+ * `general` agent is Pool's built-in and stays the user's to configure, so it
69960
+ * is never imported; an entry that is not a mapping is skipped.
69961
+ */
69962
+ function agentsOfSettings({ settings, filePath, logger }) {
69963
+ const block = settings[POOL_SUBAGENTS_KEY];
69964
+ const rawAgents = isRecord$1(block) ? block[POOL_SUBAGENTS_AGENTS_KEY] : void 0;
69965
+ const agents = {};
69966
+ if (!isRecord$1(rawAgents)) return agents;
69967
+ for (const [name, raw] of Object.entries(rawAgents)) {
69968
+ if (isPrototypePollutionKey(name) || name === "general") continue;
69969
+ const result = PoolAgentSchema.safeParse(raw);
69970
+ if (!result.success) {
69971
+ logger?.warn(`Skipped Pool subagent ${quoteValueForWarning(name)} in ${filePath}: ${formatError(result.error)}`);
69972
+ continue;
69973
+ }
69974
+ agents[name] = result.data;
69975
+ }
69976
+ return agents;
69977
+ }
69978
+ /**
69979
+ * Pool subagents.
69980
+ *
69981
+ * Pool defines subagents inside its settings file — `.poolside/settings.yaml`
69982
+ * at project scope and `~/.config/poolside/settings.yaml` at user scope —
69983
+ * under `subagents.agents.<name>`, so every targeted rulesync subagent is
69984
+ * collapsed into that one file: the body becomes `instructions`, the shared
69985
+ * `description` is written as-is and the `pool:` frontmatter section supplies
69986
+ * `type` (default `in_process`) and the type-specific keys. rulesync owns the
69987
+ * `agents` map except for Pool's built-in `general` entry, which is carried
69988
+ * over untouched together with `subagents.default` and every other top-level
69989
+ * key of the file (`mcp_servers`, `tools`, `pool`, ...); the file is never
69990
+ * deleted.
69991
+ *
69992
+ * @see https://docs.poolside.ai/subagents
69993
+ * @see https://docs.poolside.ai/settings-file-reference
69994
+ */
69995
+ var PoolSubagent = class extends ToolSubagent {
69996
+ agents;
69997
+ logger;
69998
+ /**
69999
+ * The settings document to write, built lazily: the writer hands over the
70000
+ * existing file through `setFileContent` first, and a file that does not
70001
+ * exist yet gets the block merged into an empty document. Lazy so that a
70002
+ * `forDeletion` instance never consults the gateway.
70003
+ */
70004
+ mergedContent = null;
70005
+ constructor({ agents, logger, ...rest }) {
70006
+ super({
70007
+ ...rest,
70008
+ fileContent: ""
70009
+ });
70010
+ this.agents = agents;
70011
+ this.logger = logger;
70012
+ }
70013
+ getAgents() {
70014
+ return this.agents;
70015
+ }
70016
+ isDeletable() {
70017
+ return false;
70018
+ }
70019
+ shouldMergeExistingFileContent() {
70020
+ return true;
70021
+ }
70022
+ setFileContent(newFileContent) {
70023
+ this.mergedContent = this.mergeInto(newFileContent);
70024
+ }
70025
+ getFileContent() {
70026
+ if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
70027
+ return this.mergedContent;
70028
+ }
70029
+ /**
70030
+ * Rebuild the `subagents` block on top of an existing settings document:
70031
+ * `default` and any other sibling of `agents` are kept, the built-in
70032
+ * `general` agent is carried over, and every other agent is replaced by the
70033
+ * generated set — an agent removed from `.rulesync/subagents/` must not
70034
+ * survive here. A block left empty is retracted rather than written as `{}`.
70035
+ */
70036
+ mergeInto(existingContent) {
70037
+ const paths = {
70038
+ relativeDirPath: this.getRelativeDirPath(),
70039
+ relativeFilePath: this.getRelativeFilePath()
70040
+ };
70041
+ const filePath = join(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
70042
+ const existing = parsePoolSettings(existingContent, filePath);
70043
+ const existingBlock = isRecord$1(existing["subagents"]) ? existing[POOL_SUBAGENTS_KEY] : {};
70044
+ const block = {};
70045
+ for (const [key, value] of Object.entries(existingBlock)) if (!isPrototypePollutionKey(key) && key !== "agents") block[key] = value;
70046
+ const existingAgents = isRecord$1(existingBlock["agents"]) ? existingBlock[POOL_SUBAGENTS_AGENTS_KEY] : {};
70047
+ const general = existingAgents[POOL_GENERAL_AGENT_NAME];
70048
+ const agents = {
70049
+ ...general !== void 0 ? { [POOL_GENERAL_AGENT_NAME]: general } : {},
70050
+ ...this.agents
70051
+ };
70052
+ const dropped = Object.keys(existingAgents).filter((name) => name !== "general" && !Object.hasOwn(agents, name));
70053
+ if (dropped.length > 0) {
70054
+ const advice = this.global ? "hand-written agents belong in .rulesync/subagents/" : `hand-written agents belong in ${POOL_SETTINGS_LOCAL_FILE_NAME}`;
70055
+ this.logger?.warn(`Removed Pool subagent(s) ${dropped.map(quoteValueForWarning).join(", ")} from ${filePath}: rulesync recomputes "${POOL_SUBAGENTS_KEY}.${POOL_SUBAGENTS_AGENTS_KEY}" from its own subagents on every run, so ${advice}.`);
70056
+ }
70057
+ if (Object.keys(agents).length > 0) block[POOL_SUBAGENTS_AGENTS_KEY] = agents;
70058
+ return applySharedConfigPatch({
70059
+ fileKey: sharedConfigFileKey(paths),
70060
+ feature: "subagents",
70061
+ existingContent,
70062
+ patch: { [POOL_SUBAGENTS_KEY]: Object.keys(block).length > 0 ? block : void 0 },
70063
+ filePath,
70064
+ logger: this.logger
70065
+ });
70066
+ }
70067
+ /**
70068
+ * Every agent lives in the one settings file, which the MCP feature writes
70069
+ * too: naming the file here keeps the gitignore derivation from claiming the
70070
+ * whole directory and lets the shared-file derivation order the writers.
70071
+ */
70072
+ static getSettablePaths({ global = false } = {}) {
70073
+ return {
70074
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
70075
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
70076
+ };
70077
+ }
70078
+ /**
70079
+ * Map a single rulesync subagent to a Pool agent entry. The body becomes
70080
+ * `instructions` and the shared `description` is written as-is; the
70081
+ * optional `pool:` section supplies `type` and the type-specific keys and
70082
+ * may override both. Returns `null` for the reserved `general` name, which
70083
+ * Pool defines itself.
70084
+ */
70085
+ static toPoolAgent({ rulesyncSubagent, logger }) {
70086
+ const frontmatter = rulesyncSubagent.getFrontmatter();
70087
+ const name = frontmatter.name;
70088
+ if (name === "general") {
70089
+ logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: "${POOL_GENERAL_AGENT_NAME}" is Pool's built-in agent, so configure it in settings.yaml directly.`);
70090
+ return null;
70091
+ }
70092
+ if (isPrototypePollutionKey(name)) {
70093
+ logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: unsafe name.`);
70094
+ return null;
70095
+ }
70096
+ const rawSection = isRecord$1(frontmatter.pool) ? frontmatter.pool : {};
70097
+ const result = PoolAgentSchema.safeParse(rawSection);
70098
+ if (!result.success) throw new Error(`Invalid "pool" section of subagent ${quoteValueForWarning(name)}: ${formatError(result.error)}`);
70099
+ const { type, description, instructions, ...rest } = result.data;
70100
+ const body = rulesyncSubagent.getBody().trim();
70101
+ const effectiveDescription = description ?? frontmatter.description;
70102
+ const effectiveInstructions = instructions ?? (body !== "" ? body : void 0);
70103
+ if (effectiveDescription === void 0 && rest.disabled !== true) logger?.warn(`Pool subagent ${quoteValueForWarning(name)} has no description; Pool requires one for every enabled custom agent.`);
70104
+ return {
70105
+ name,
70106
+ agent: {
70107
+ type: type ?? DEFAULT_POOL_AGENT_TYPE,
70108
+ ...effectiveDescription !== void 0 ? { description: effectiveDescription } : {},
70109
+ ...effectiveInstructions !== void 0 ? { instructions: effectiveInstructions } : {},
70110
+ ...rest
70111
+ }
70112
+ };
70113
+ }
70114
+ /**
70115
+ * Aggregate every targeted rulesync subagent into the one settings file.
70116
+ * Agents are keyed by their `name`; a repeated name keeps the last one, with
70117
+ * a warning, so the map stays what Pool would read.
70118
+ */
70119
+ static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true, global = false, logger }) {
70120
+ const agents = {};
70121
+ for (const rulesyncSubagent of rulesyncSubagents) {
70122
+ const converted = this.toPoolAgent({
70123
+ rulesyncSubagent,
70124
+ logger
70125
+ });
70126
+ if (converted === null) continue;
70127
+ if (Object.hasOwn(agents, converted.name)) logger?.warn(`Pool subagent ${quoteValueForWarning(converted.name)} is defined more than once; the last definition (${rulesyncSubagent.getRelativePathFromCwd()}) wins.`);
70128
+ agents[converted.name] = converted.agent;
70129
+ }
70130
+ return new this({
70131
+ outputRoot,
70132
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
70133
+ relativeFilePath: POOL_SETTINGS_FILE_NAME,
70134
+ agents,
70135
+ validate,
70136
+ global,
70137
+ logger
70138
+ });
70139
+ }
70140
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
70141
+ return this.fromRulesyncSubagents({
70142
+ outputRoot,
70143
+ rulesyncSubagents: [rulesyncSubagent],
70144
+ validate,
70145
+ global,
70146
+ logger
70147
+ });
70148
+ }
70149
+ /**
70150
+ * Convert every custom agent of the settings file back into an individual
70151
+ * rulesync subagent. `instructions` becomes the body, `description` the
70152
+ * shared field, and everything else — `type` only when it is not the
70153
+ * default — rides the `pool:` section so it survives the round-trip.
70154
+ */
70155
+ toRulesyncSubagents() {
70156
+ return Object.entries(this.agents).map(([name, agent]) => {
70157
+ const { type, description, instructions, ...rest } = agent;
70158
+ const poolSection = {
70159
+ ...type !== void 0 && type !== DEFAULT_POOL_AGENT_TYPE ? { type } : {},
70160
+ ...rest
70161
+ };
70162
+ const rulesyncFrontmatter = {
70163
+ targets: ["pool"],
70164
+ name,
70165
+ ...description !== void 0 ? { description } : {},
70166
+ ...Object.keys(poolSection).length > 0 ? { pool: poolSection } : {}
70167
+ };
70168
+ return new RulesyncSubagent({
70169
+ outputRoot: process.cwd(),
70170
+ frontmatter: rulesyncFrontmatter,
70171
+ body: instructions ?? "",
70172
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
70173
+ relativeFilePath: `${sanitizePoolAgentFileStem(name)}.md`,
70174
+ validate: true
70175
+ });
70176
+ });
70177
+ }
70178
+ toRulesyncSubagent() {
70179
+ const first = this.toRulesyncSubagents()[0];
70180
+ if (!first) throw new Error(`No custom subagents found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())} to convert.`);
70181
+ return first;
70182
+ }
70183
+ validate() {
70184
+ return {
70185
+ success: true,
70186
+ error: null
70187
+ };
70188
+ }
70189
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
70190
+ return this.isTargetedByRulesyncSubagentDefault({
70191
+ rulesyncSubagent,
70192
+ toolTarget: "pool"
70193
+ });
70194
+ }
70195
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, logger }) {
70196
+ const paths = this.getSettablePaths({ global });
70197
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
70198
+ const settings = parsePoolSettings(await readFileContentOrNull(filePath) ?? "", filePath);
70199
+ return new this({
70200
+ outputRoot,
70201
+ relativeDirPath: paths.relativeDirPath,
70202
+ relativeFilePath,
70203
+ agents: agentsOfSettings({
70204
+ settings,
70205
+ filePath,
70206
+ logger
70207
+ }),
70208
+ validate,
70209
+ global
70210
+ });
70211
+ }
70212
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
70213
+ return new this({
70214
+ outputRoot,
70215
+ relativeDirPath,
70216
+ relativeFilePath,
70217
+ agents: {},
70218
+ validate: false,
70219
+ global
70220
+ });
70221
+ }
70222
+ };
70223
+ //#endregion
68470
70224
  //#region src/features/subagents/reasonix-subagent.ts
68471
70225
  const ReasonixSubagentFrontmatterSchema = z.looseObject({
68472
70226
  name: z.string(),
@@ -69787,6 +71541,16 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
69787
71541
  filePattern: "*.md"
69788
71542
  }
69789
71543
  }],
71544
+ ["pool", {
71545
+ class: PoolSubagent,
71546
+ meta: {
71547
+ supportsProject: true,
71548
+ supportsSimulated: false,
71549
+ supportsGlobal: true,
71550
+ emitsEmptyAggregate: true,
71551
+ filePattern: POOL_SETTINGS_FILE_NAME
71552
+ }
71553
+ }],
69790
71554
  ["qwencode", {
69791
71555
  class: QwencodeSubagent,
69792
71556
  meta: {
@@ -69899,6 +71663,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
69899
71663
  toolTarget;
69900
71664
  global;
69901
71665
  getFactory;
71666
+ /**
71667
+ * Whether the last `loadRulesyncFiles` found a `subagents/` directory under
71668
+ * any input root. A project without one has not adopted the feature, so the
71669
+ * agents in its Pool settings are its own and must not be retracted.
71670
+ */
71671
+ rulesyncSourceDirFound = false;
69902
71672
  constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
69903
71673
  super({
69904
71674
  outputRoot,
@@ -69912,16 +71682,20 @@ var SubagentsProcessor = class extends FeatureProcessor {
69912
71682
  this.global = global;
69913
71683
  this.getFactory = getFactory;
69914
71684
  }
71685
+ emitsToolFilesForEmptySource() {
71686
+ return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
71687
+ }
69915
71688
  async convertRulesyncFilesToToolFiles(rulesyncFiles) {
69916
71689
  const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
69917
71690
  const factory = this.getFactory(this.toolTarget);
69918
71691
  const targeted = rulesyncSubagents.filter((rulesyncSubagent) => factory.class.isTargetedByRulesyncSubagent(rulesyncSubagent));
69919
71692
  if (factory.class.fromRulesyncSubagents) {
69920
- if (targeted.length === 0) return [];
71693
+ if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
69921
71694
  const toolSubagents = factory.class.fromRulesyncSubagents({
69922
71695
  outputRoot: this.outputRoot,
69923
71696
  rulesyncSubagents: targeted,
69924
- global: this.global
71697
+ global: this.global,
71698
+ logger: this.logger
69925
71699
  });
69926
71700
  return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
69927
71701
  }
@@ -69977,6 +71751,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
69977
71751
  this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
69978
71752
  return [];
69979
71753
  }
71754
+ this.rulesyncSourceDirFound = true;
69980
71755
  const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
69981
71756
  if (mdFiles.length === 0) {
69982
71757
  this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
@@ -70017,6 +71792,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
70017
71792
  * earlier root's copy.
70018
71793
  */
70019
71794
  async loadRulesyncFiles() {
71795
+ this.rulesyncSourceDirFound = false;
70020
71796
  const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
70021
71797
  perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
70022
71798
  identity: (subagent) => subagent.getRelativeFilePath(),
@@ -70079,7 +71855,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
70079
71855
  outputRoot: rootOutputRoot,
70080
71856
  relativeDirPath: dirPath,
70081
71857
  relativeFilePath: toRelativeFilePath(path),
70082
- global: this.global
71858
+ global: this.global,
71859
+ logger: this.logger
70083
71860
  })));
70084
71861
  toolSubagents.push(...this.claimStandaloneSubagents({
70085
71862
  loaded,
@@ -74001,22 +75778,30 @@ var DevinRule = class DevinRule extends ToolRule {
74001
75778
  }
74002
75779
  };
74003
75780
  //#endregion
74004
- //#region src/features/rules/dsh-rule.ts
75781
+ //#region src/features/rules/nested-agentsmd-rule.ts
74005
75782
  /**
74006
- * DeepSeek Harness (`dsh`) rules.
75783
+ * Shared adapter for the targets that read instructions exactly the way the
75784
+ * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
75785
+ * project root, nested per-directory `AGENTS.md` files loaded only while working
75786
+ * under that directory, and a single personal root file under a tool-specific
75787
+ * home directory. Such a target has no modular non-root instruction directory,
75788
+ * so every other non-root rule folds into the root file.
74007
75789
  *
74008
- * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
74009
- * files — the project chain `dsh-agent-instructions` loads from the `.git`
74010
- * root down to the session cwd. Global scope writes the user-global
74011
- * `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
74012
- * single root file with no local overlay, so non-root rules fold into it.
75790
+ * Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
75791
+ * directory and their tool target through {@link getFamily}; the vendor docs
75792
+ * describing each one's discovery walk live on the subclass.
74013
75793
  *
74014
- * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
75794
+ * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
75795
+ * same file name, same exclusions, import-only, project scope — because it
75796
+ * discovers literally the same files.
74015
75797
  */
74016
- var DshRule = class DshRule extends ToolRule {
75798
+ var NestedAgentsmdRule = class extends ToolRule {
75799
+ static getFamily() {
75800
+ throw new Error("Please implement this method in the subclass.");
75801
+ }
74017
75802
  static getSettablePaths({ global = false } = {}) {
74018
75803
  if (global) return { root: {
74019
- relativeDirPath: DSH_DIR,
75804
+ relativeDirPath: this.getFamily().globalDir,
74020
75805
  relativeFilePath: AGENTSMD_RULE_FILE_NAME
74021
75806
  } };
74022
75807
  return { root: {
@@ -74024,18 +75809,6 @@ var DshRule = class DshRule extends ToolRule {
74024
75809
  relativeFilePath: AGENTSMD_RULE_FILE_NAME
74025
75810
  } };
74026
75811
  }
74027
- /**
74028
- * DeepSeek Harness loads the project chain of `AGENTS.md` files from the
74029
- * project root (the nearest ancestor holding `.git`) down through the session
74030
- * cwd, broad to specific, so a per-directory file applies only while working
74031
- * under that directory. Nested files are therefore a real scoping surface,
74032
- * not just the root file's overflow.
74033
- *
74034
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
74035
- * name, same exclusions, import-only, project scope — because it discovers
74036
- * literally the same files.
74037
- * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
74038
- */
74039
75812
  static getNestedFilePatterns() {
74040
75813
  return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
74041
75814
  }
@@ -74050,7 +75823,7 @@ var DshRule = class DshRule extends ToolRule {
74050
75823
  const { root } = this.getSettablePaths({ global });
74051
75824
  if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
74052
75825
  const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
74053
- return new DshRule({
75826
+ return new this({
74054
75827
  outputRoot,
74055
75828
  relativeDirPath: overrideDirPath,
74056
75829
  relativeFilePath: AGENTSMD_RULE_FILE_NAME,
@@ -74060,7 +75833,7 @@ var DshRule = class DshRule extends ToolRule {
74060
75833
  });
74061
75834
  }
74062
75835
  const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
74063
- return new DshRule({
75836
+ return new this({
74064
75837
  outputRoot,
74065
75838
  relativeDirPath: root.relativeDirPath,
74066
75839
  relativeFilePath: root.relativeFilePath,
@@ -74074,7 +75847,7 @@ var DshRule = class DshRule extends ToolRule {
74074
75847
  const frontmatter = rulesyncRule.getFrontmatter();
74075
75848
  const isRoot = frontmatter.root ?? false;
74076
75849
  const subprojectPath = frontmatter.agentsmd?.subprojectPath;
74077
- if (!global && !isRoot && subprojectPath) return new DshRule({
75850
+ if (!global && !isRoot && subprojectPath) return new this({
74078
75851
  outputRoot,
74079
75852
  relativeDirPath: join(subprojectPath),
74080
75853
  relativeFilePath: AGENTSMD_RULE_FILE_NAME,
@@ -74082,7 +75855,7 @@ var DshRule = class DshRule extends ToolRule {
74082
75855
  validate,
74083
75856
  root: false
74084
75857
  });
74085
- return new DshRule({
75858
+ return new this({
74086
75859
  outputRoot,
74087
75860
  relativeDirPath: root.relativeDirPath,
74088
75861
  relativeFilePath: root.relativeFilePath,
@@ -74103,23 +75876,47 @@ var DshRule = class DshRule extends ToolRule {
74103
75876
  };
74104
75877
  }
74105
75878
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
74106
- return new DshRule({
75879
+ const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
75880
+ return new this({
74107
75881
  outputRoot,
74108
75882
  relativeDirPath,
74109
75883
  relativeFilePath,
74110
75884
  fileContent: "",
74111
75885
  validate: false,
74112
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".dsh")
75886
+ root: isRoot
74113
75887
  });
74114
75888
  }
74115
75889
  static isTargetedByRulesyncRule(rulesyncRule) {
74116
75890
  return this.isTargetedByRulesyncRuleDefault({
74117
75891
  rulesyncRule,
74118
- toolTarget: "dsh"
75892
+ toolTarget: this.getFamily().toolTarget
74119
75893
  });
74120
75894
  }
74121
75895
  };
74122
75896
  //#endregion
75897
+ //#region src/features/rules/dsh-rule.ts
75898
+ /**
75899
+ * DeepSeek Harness (`dsh`) rules.
75900
+ *
75901
+ * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
75902
+ * files — the project chain `dsh-agent-instructions` loads from the `.git`
75903
+ * root down to the session cwd, broad to specific, so a per-directory file
75904
+ * applies only while working under that directory. Global scope writes the
75905
+ * user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
75906
+ * reads as a single root file with no `.local.md` overlay, so non-root rules
75907
+ * fold into it.
75908
+ *
75909
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
75910
+ */
75911
+ var DshRule = class extends NestedAgentsmdRule {
75912
+ static getFamily() {
75913
+ return {
75914
+ globalDir: DSH_DIR,
75915
+ toolTarget: "dsh"
75916
+ };
75917
+ }
75918
+ };
75919
+ //#endregion
74123
75920
  //#region src/features/rules/factorydroid-rule.ts
74124
75921
  /**
74125
75922
  * Rule generator for Factory Droid.
@@ -75513,110 +77310,24 @@ var PiRule = class PiRule extends ToolRule {
75513
77310
  };
75514
77311
  //#endregion
75515
77312
  //#region src/features/rules/pool-rule.ts
75516
- var PoolRule = class PoolRule extends ToolRule {
75517
- static getSettablePaths({ global = false } = {}) {
75518
- if (global) return { root: {
75519
- relativeDirPath: POOL_GLOBAL_DIR,
75520
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
75521
- } };
75522
- return { root: {
75523
- relativeDirPath: ".",
75524
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
75525
- } };
75526
- }
75527
- /**
75528
- * Pool reads personal, project, and directory-level `AGENTS.md` files: inside
75529
- * a git repository it loads every `AGENTS.md` from the repository root down
75530
- * through the working directory (deeper files take precedence), skipping
75531
- * ignored directories. Nested files are therefore a real scoping surface, not
75532
- * just the root file's overflow.
75533
- *
75534
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
75535
- * name, same exclusions, import-only, project scope — because it discovers
75536
- * literally the same files.
75537
- * @see https://docs.poolside.ai/agent-instructions
75538
- */
75539
- static getNestedFilePatterns() {
75540
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
75541
- }
75542
- /**
75543
- * The subproject directory this rule scopes, or `undefined` for the root file
75544
- * (project or global).
75545
- */
75546
- getSubprojectPath() {
75547
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
75548
- }
75549
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
75550
- const { root } = this.getSettablePaths({ global });
75551
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
75552
- const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
75553
- return new PoolRule({
75554
- outputRoot,
75555
- relativeDirPath: overrideDirPath,
75556
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75557
- fileContent,
75558
- validate,
75559
- root: false
75560
- });
75561
- }
75562
- const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
75563
- return new PoolRule({
75564
- outputRoot,
75565
- relativeDirPath: root.relativeDirPath,
75566
- relativeFilePath: root.relativeFilePath,
75567
- fileContent,
75568
- validate,
75569
- root: true
75570
- });
75571
- }
75572
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
75573
- const { root } = this.getSettablePaths({ global });
75574
- const frontmatter = rulesyncRule.getFrontmatter();
75575
- const isRoot = frontmatter.root ?? false;
75576
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
75577
- if (!global && !isRoot && subprojectPath) return new PoolRule({
75578
- outputRoot,
75579
- relativeDirPath: join(subprojectPath),
75580
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75581
- fileContent: rulesyncRule.getBody(),
75582
- validate,
75583
- root: false
75584
- });
75585
- return new PoolRule({
75586
- outputRoot,
75587
- relativeDirPath: root.relativeDirPath,
75588
- relativeFilePath: root.relativeFilePath,
75589
- fileContent: rulesyncRule.getBody(),
75590
- validate,
75591
- root: isRoot
75592
- });
75593
- }
75594
- toRulesyncRule() {
75595
- const subprojectPath = this.getSubprojectPath();
75596
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
75597
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
75598
- }
75599
- validate() {
77313
+ /**
77314
+ * Pool (Poolside) rules.
77315
+ *
77316
+ * Pool reads personal, project, and directory-level `AGENTS.md` files: inside
77317
+ * a git repository it loads every `AGENTS.md` from the repository root down
77318
+ * through the working directory (deeper files take precedence), skipping
77319
+ * ignored directories. Nested files are therefore a real scoping surface, not
77320
+ * just the root file's overflow. The personal file sits below both at
77321
+ * `~/.config/poolside/AGENTS.md`.
77322
+ *
77323
+ * @see https://docs.poolside.ai/agent-instructions
77324
+ */
77325
+ var PoolRule = class extends NestedAgentsmdRule {
77326
+ static getFamily() {
75600
77327
  return {
75601
- success: true,
75602
- error: null
75603
- };
75604
- }
75605
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
75606
- return new PoolRule({
75607
- outputRoot,
75608
- relativeDirPath,
75609
- relativeFilePath,
75610
- fileContent: "",
75611
- validate: false,
75612
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === POOL_GLOBAL_DIR)
75613
- });
75614
- }
75615
- static isTargetedByRulesyncRule(rulesyncRule) {
75616
- return this.isTargetedByRulesyncRuleDefault({
75617
- rulesyncRule,
77328
+ globalDir: POOL_GLOBAL_DIR,
75618
77329
  toolTarget: "pool"
75619
- });
77330
+ };
75620
77331
  }
75621
77332
  };
75622
77333
  //#endregion
@@ -76580,110 +78291,23 @@ var TaktRule = class TaktRule extends ToolRule {
76580
78291
  //#endregion
76581
78292
  //#region src/features/rules/vibe-rule.ts
76582
78293
  const VIBE_GLOBAL_DIR = ".vibe";
76583
- var VibeRule = class VibeRule extends ToolRule {
76584
- static getSettablePaths({ global = false } = {}) {
76585
- if (global) return { root: {
76586
- relativeDirPath: VIBE_GLOBAL_DIR,
76587
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76588
- } };
76589
- return { root: {
76590
- relativeDirPath: ".",
76591
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76592
- } };
76593
- }
76594
- /**
76595
- * Vibe's harness manager walks the directories between the workspace root and
76596
- * the file being read and loads every `AGENTS.md` it finds along the way
76597
- * (`find_subdirectory_agents_md`), injecting the result into the `read_file`
76598
- * tool's output. Nested files are therefore a real scoping surface, not just
76599
- * the root file's overflow.
76600
- *
76601
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
76602
- * name, same exclusions, import-only, project scope — because it discovers
76603
- * literally the same files.
76604
- * @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
76605
- */
76606
- static getNestedFilePatterns() {
76607
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
76608
- }
76609
- /**
76610
- * The subproject directory this rule scopes, or `undefined` for the root file
76611
- * (project or global).
76612
- */
76613
- getSubprojectPath() {
76614
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
76615
- }
76616
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
76617
- const { root } = this.getSettablePaths({ global });
76618
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
76619
- const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
76620
- return new VibeRule({
76621
- outputRoot,
76622
- relativeDirPath: overrideDirPath,
76623
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76624
- fileContent,
76625
- validate,
76626
- root: false
76627
- });
76628
- }
76629
- const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
76630
- return new VibeRule({
76631
- outputRoot,
76632
- relativeDirPath: root.relativeDirPath,
76633
- relativeFilePath: root.relativeFilePath,
76634
- fileContent,
76635
- validate,
76636
- root: true
76637
- });
76638
- }
76639
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
76640
- const { root } = this.getSettablePaths({ global });
76641
- const frontmatter = rulesyncRule.getFrontmatter();
76642
- const isRoot = frontmatter.root ?? false;
76643
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
76644
- if (!global && !isRoot && subprojectPath) return new VibeRule({
76645
- outputRoot,
76646
- relativeDirPath: join(subprojectPath),
76647
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76648
- fileContent: rulesyncRule.getBody(),
76649
- validate,
76650
- root: false
76651
- });
76652
- return new VibeRule({
76653
- outputRoot,
76654
- relativeDirPath: root.relativeDirPath,
76655
- relativeFilePath: root.relativeFilePath,
76656
- fileContent: rulesyncRule.getBody(),
76657
- validate,
76658
- root: isRoot
76659
- });
76660
- }
76661
- toRulesyncRule() {
76662
- const subprojectPath = this.getSubprojectPath();
76663
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
76664
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
76665
- }
76666
- validate() {
78294
+ /**
78295
+ * Mistral Vibe rules.
78296
+ *
78297
+ * Vibe's harness manager walks the directories between the workspace root and
78298
+ * the file being read and loads every `AGENTS.md` it finds along the way
78299
+ * (`find_subdirectory_agents_md`), injecting the result into the `read_file`
78300
+ * tool's output. Nested files are therefore a real scoping surface, not just
78301
+ * the root file's overflow. The personal file is `~/.vibe/AGENTS.md`.
78302
+ *
78303
+ * @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
78304
+ */
78305
+ var VibeRule = class extends NestedAgentsmdRule {
78306
+ static getFamily() {
76667
78307
  return {
76668
- success: true,
76669
- error: null
76670
- };
76671
- }
76672
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
76673
- return new VibeRule({
76674
- outputRoot,
76675
- relativeDirPath,
76676
- relativeFilePath,
76677
- fileContent: "",
76678
- validate: false,
76679
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".vibe")
76680
- });
76681
- }
76682
- static isTargetedByRulesyncRule(rulesyncRule) {
76683
- return this.isTargetedByRulesyncRuleDefault({
76684
- rulesyncRule,
78308
+ globalDir: VIBE_GLOBAL_DIR,
76685
78309
  toolTarget: "vibe"
76686
- });
78310
+ };
76687
78311
  }
76688
78312
  };
76689
78313
  //#endregion
@@ -78890,6 +80514,16 @@ async function activateHermesProjectPlugins({ pluginNames, dryRun, logger }) {
78890
80514
  relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
78891
80515
  });
78892
80516
  const configPath = join(configRoot, relativeConfigPath);
80517
+ if (await refusesWriteOutsideRoot({
80518
+ logger,
80519
+ rootPath: configRoot,
80520
+ targetPath: configPath
80521
+ })) return {
80522
+ count: 0,
80523
+ paths: [],
80524
+ hasDiff: false,
80525
+ sourceLoadFailed: false
80526
+ };
78893
80527
  const expectedConfig = mergeEnabledPlugins({
78894
80528
  existingContent: await readFileContentOrNull(configPath) ?? "",
78895
80529
  filePath: configPath,
@@ -79357,7 +80991,8 @@ async function processEmptyFeatureGeneration(params) {
79357
80991
  */
79358
80992
  async function processFeatureWithRulesyncFiles(params) {
79359
80993
  const { config, processor, rulesyncFiles, sweepPlan, skipFilePaths, onToolFiles } = params;
79360
- if (rulesyncFiles.length === 0) return processEmptyFeatureGeneration({
80994
+ const retractsForEmptySource = processor.emitsToolFilesForEmptySource() && !processor.hasRulesyncSourceLoadFailure();
80995
+ if (rulesyncFiles.length === 0 && !retractsForEmptySource) return processEmptyFeatureGeneration({
79361
80996
  config,
79362
80997
  processor,
79363
80998
  sweepPlan,
@@ -80589,4 +82224,4 @@ async function importChecksCore(params) {
80589
82224
  //#endregion
80590
82225
  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 };
80591
82226
 
80592
- //# sourceMappingURL=import-BnF2J75x.js.map
82227
+ //# sourceMappingURL=import-B2ANxi1V.js.map