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.
@@ -444,36 +444,36 @@ const isFeatureValueEnabled = (value) => {
444
444
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
445
445
  //#endregion
446
446
  //#region src/constants/rulesync-paths.ts
447
- const { join: join$350 } = node_path.posix;
447
+ const { join: join$352 } = node_path.posix;
448
448
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
449
449
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
450
450
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
451
451
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
452
452
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
453
453
  const RULES_FEATURE_SUBDIR = "rules";
454
- const CURATED_RULES_FEATURE_SUBDIR = join$350(RULES_FEATURE_SUBDIR, ".curated");
454
+ const CURATED_RULES_FEATURE_SUBDIR = join$352(RULES_FEATURE_SUBDIR, ".curated");
455
455
  const COMMANDS_FEATURE_SUBDIR = "commands";
456
456
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
457
457
  const CHECKS_FEATURE_SUBDIR = "checks";
458
458
  const SKILLS_FEATURE_SUBDIR = "skills";
459
- const CURATED_SKILLS_FEATURE_SUBDIR = join$350(SKILLS_FEATURE_SUBDIR, ".curated");
460
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
- join$350(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
459
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$352(SKILLS_FEATURE_SUBDIR, ".curated");
460
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
461
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
462
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
463
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
464
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
465
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
466
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
467
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
468
+ join$352(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
469
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
470
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
471
471
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
472
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
472
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
473
473
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
474
474
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
475
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$350(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
475
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
476
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
477
477
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
478
478
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
479
479
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -616,6 +616,7 @@ const mcpProcessorToolTargetTuple = [
616
616
  "junie",
617
617
  "musecode",
618
618
  "opencode",
619
+ "pool",
619
620
  "qwencode",
620
621
  "reasonix",
621
622
  "roo",
@@ -695,6 +696,7 @@ const subagentsProcessorToolTargetTuple = [
695
696
  "kiro-cli",
696
697
  "kiro-ide",
697
698
  "opencode",
699
+ "pool",
698
700
  "qwencode",
699
701
  "reasonix",
700
702
  "roo",
@@ -776,6 +778,7 @@ const hooksProcessorToolTargetTuple = [
776
778
  "crush",
777
779
  "opencode",
778
780
  "pi",
781
+ "pool",
779
782
  "factorydroid",
780
783
  "goose",
781
784
  "hermesagent",
@@ -823,6 +826,7 @@ const permissionsProcessorToolTargetTuple = [
823
826
  "kiro-ide",
824
827
  "opencode",
825
828
  "pi",
829
+ "pool",
826
830
  "qwencode",
827
831
  "reasonix",
828
832
  "roo",
@@ -1379,6 +1383,111 @@ async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
1379
1383
  }));
1380
1384
  }
1381
1385
  /**
1386
+ * How many links {@link writeLandingPath} follows before it treats the chain as
1387
+ * a cycle it cannot write through. Linux itself gives up after 40.
1388
+ */
1389
+ const MAX_LINK_HOPS = 40;
1390
+ /**
1391
+ * Whether `error` is the ENOTDIR a lookup through a file raises. On the way to
1392
+ * a write it means the same as a missing entry: nothing at or below the path
1393
+ * exists, so nothing there can be a link.
1394
+ */
1395
+ function isNotADirectoryError(error) {
1396
+ return someErrorInChain(error, (candidate) => "code" in candidate && candidate.code === "ENOTDIR");
1397
+ }
1398
+ /**
1399
+ * Where a write to `targetPath` lands once the OS has followed every link on
1400
+ * the way there, or `null` when the links form a cycle no write can get
1401
+ * through. The target itself usually does not exist yet, so the walk goes
1402
+ * segment by segment from the filesystem root: an existing segment that is a
1403
+ * link is replaced by what it resolves to, and a dangling link — one whose
1404
+ * target does not exist yet, which is exactly the shape of a link planted at
1405
+ * `AGENTS.md` pointing at a `~/.zshenv` that is still to be created — has its
1406
+ * target spliced into the walk in its place, so the segments of that target
1407
+ * are looked up one by one too. Once a segment is missing, nothing below it
1408
+ * exists, and the rest of the path is where the write's `mkdir -p` will put it.
1409
+ *
1410
+ * Walking segment by segment is what keeps a `..` honest. A link target is a
1411
+ * path the OS resolves left to right, so in `sub/../victim` the `..` climbs out
1412
+ * of wherever `sub` leads, not out of the directory `sub` is spelled in; a
1413
+ * lexical `resolve` folds `sub/..` away before `sub` is ever looked at, which
1414
+ * is how a link chain could be spelled to pass the check and still land
1415
+ * outside the root.
1416
+ */
1417
+ async function writeLandingPath(targetPath) {
1418
+ const start = splitAbsolutePath((0, node_path.isAbsolute)(targetPath) ? targetPath : (0, node_path.resolve)(targetPath));
1419
+ let current = start.root;
1420
+ let pending = start.segments;
1421
+ let exists = true;
1422
+ let linkHops = 0;
1423
+ while (pending.length > 0) {
1424
+ const segment = pending.shift();
1425
+ if (segment === void 0 || segment === "" || segment === ".") continue;
1426
+ if (segment === "..") {
1427
+ current = (0, node_path.dirname)(current);
1428
+ continue;
1429
+ }
1430
+ current = (0, node_path.join)(current, segment);
1431
+ if (!exists) continue;
1432
+ let stats;
1433
+ try {
1434
+ stats = await (0, node_fs_promises.lstat)(current);
1435
+ } catch (error) {
1436
+ if (!isFileNotFoundError(error) && !isNotADirectoryError(error)) throw error;
1437
+ exists = false;
1438
+ continue;
1439
+ }
1440
+ if (!stats.isSymbolicLink()) continue;
1441
+ linkHops += 1;
1442
+ if (linkHops > MAX_LINK_HOPS) return null;
1443
+ try {
1444
+ current = await (0, node_fs_promises.realpath)(current);
1445
+ continue;
1446
+ } catch {}
1447
+ const linkTarget = await (0, node_fs_promises.readlink)(current);
1448
+ if ((0, node_path.isAbsolute)(linkTarget)) {
1449
+ const target = splitAbsolutePath(linkTarget);
1450
+ current = target.root;
1451
+ pending = [...target.segments, ...pending];
1452
+ } else {
1453
+ current = (0, node_path.dirname)(current);
1454
+ pending = [...splitPathSegments(linkTarget), ...pending];
1455
+ }
1456
+ }
1457
+ return current;
1458
+ }
1459
+ /**
1460
+ * Split an absolute path into its root and its raw segments, without folding a
1461
+ * `..` away: {@link writeLandingPath} has to see every `..` where it is spelled.
1462
+ */
1463
+ function splitAbsolutePath(absolutePath) {
1464
+ const root = (0, node_path.parse)(absolutePath).root;
1465
+ return {
1466
+ root,
1467
+ segments: splitPathSegments(absolutePath.slice(root.length))
1468
+ };
1469
+ }
1470
+ /**
1471
+ * Whether writing `targetPath` would land outside `rootPath` once every link on
1472
+ * the way there is resolved. Both sides are taken to where a write to them
1473
+ * would land (see {@link writeLandingPath}), so a root that does not fully
1474
+ * exist yet is judged the same way as the target below it.
1475
+ *
1476
+ * A link that stays inside the root passes. That is what sets this apart from
1477
+ * the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
1478
+ * dotfiles checkout linked from inside the home directory is a common shape for
1479
+ * a global write target, and refusing it would refuse `--global` outright.
1480
+ *
1481
+ * A link chain that cycles is reported as an escape: the write could not get
1482
+ * through it, and refusing it is the answer that never writes somewhere
1483
+ * unexpected.
1484
+ */
1485
+ async function writablePathEscapesRoot({ rootPath, targetPath }) {
1486
+ const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
1487
+ if (rootLanding === null || targetLanding === null) return true;
1488
+ return pathEscapesRoot((0, node_path.relative)(rootLanding, targetLanding));
1489
+ }
1490
+ /**
1382
1491
  * How many trailing segments `filePath` and `identity` have in common.
1383
1492
  *
1384
1493
  * A path that walked through no link at all shares all of its own segments with
@@ -3329,6 +3438,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3329
3438
  ".grok/config.toml",
3330
3439
  ".rovodev/config.yml",
3331
3440
  ".rovodev/mcp.json",
3441
+ ".poolside/settings.yaml",
3442
+ ".config/poolside/settings.yaml",
3332
3443
  ".tabnine/agent/settings.json",
3333
3444
  ".vibe/config.toml",
3334
3445
  ".zcode/config.json",
@@ -4378,7 +4489,7 @@ const PI_HOOK_EVENTS = [
4378
4489
  * adapter. Amp's `agent.start` / `agent.end` events describe the main agent
4379
4490
  * turn, not a subagent lifecycle, so they map to `beforeSubmitPrompt` / `stop`.
4380
4491
  *
4381
- * @see https://ampcode.com/manual/plugin-api
4492
+ * @see https://ampcode.com/docs/plugin-api
4382
4493
  */
4383
4494
  const AMP_HOOK_EVENTS = [
4384
4495
  "sessionStart",
@@ -4676,6 +4787,34 @@ const VIBE_HOOK_EVENTS = [
4676
4787
  "stop"
4677
4788
  ];
4678
4789
  /**
4790
+ * Hook events supported by Pool.
4791
+ *
4792
+ * Pool's `hooks` settings block fires six events, spelled the Claude Code
4793
+ * way: `PreToolUse`, `PostToolUse`, `UserPromptSubmit` (← `beforeSubmitPrompt`),
4794
+ * `PreCompact`, `SessionStart` and `Stop`. Every entry carries a `matcher`
4795
+ * (required by Pool's schema; `""`/`*` match any tool, a bare name is an exact
4796
+ * match, `a|b` lists names, anything else is a regular expression), which only
4797
+ * `PreToolUse`/`PostToolUse` test against the tool name.
4798
+ * @see https://docs.poolside.ai/hooks
4799
+ */
4800
+ const POOL_HOOK_EVENTS = [
4801
+ "preToolUse",
4802
+ "postToolUse",
4803
+ "beforeSubmitPrompt",
4804
+ "preCompact",
4805
+ "sessionStart",
4806
+ "stop"
4807
+ ];
4808
+ const CANONICAL_TO_POOL_EVENT_NAMES = {
4809
+ preToolUse: "PreToolUse",
4810
+ postToolUse: "PostToolUse",
4811
+ beforeSubmitPrompt: "UserPromptSubmit",
4812
+ preCompact: "PreCompact",
4813
+ sessionStart: "SessionStart",
4814
+ stop: "Stop"
4815
+ };
4816
+ const POOL_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_EVENT_NAMES).map(([k, v]) => [v, k]));
4817
+ /**
4679
4818
  * Hook events supported by Crush.
4680
4819
  *
4681
4820
  * Crush's `hooks` config block currently fires a single event, `PreToolUse`
@@ -5270,6 +5409,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
5270
5409
  "kimi-code": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5271
5410
  zcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5272
5411
  crush: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5412
+ pool: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5273
5413
  bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5274
5414
  cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5275
5415
  commandcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
@@ -6223,6 +6363,7 @@ const McpServerSchema = zod_mini.z.looseObject({
6223
6363
  crushOauthClientSecret: zod_mini.z.optional(zod_mini.z.string()),
6224
6364
  crushOauthCallbackPort: zod_mini.z.optional(zod_mini.z.int()),
6225
6365
  crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
6366
+ poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
6226
6367
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
6227
6368
  /**
6228
6369
  * The canonical per-server tool allowlist.
@@ -6654,6 +6795,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
6654
6795
  "crushOauthClientSecret",
6655
6796
  "crushOauthCallbackPort",
6656
6797
  "crushSessionless",
6798
+ "poolAllow",
6657
6799
  "oauth_client_id",
6658
6800
  "oauth_client_secret",
6659
6801
  "oauth_callback_port",
@@ -7996,6 +8138,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
7996
8138
  copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7997
8139
  crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7998
8140
  goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8141
+ pool: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
7999
8142
  grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
8000
8143
  "kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
8001
8144
  roo: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
@@ -8341,7 +8484,7 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
8341
8484
  * them; treating a segment that carries one as static could name a directory
8342
8485
  * that no matched file actually lives under.
8343
8486
  */
8344
- const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
8487
+ const GLOB_METACHARACTERS$1 = /[*?[\]{}()!]/;
8345
8488
  /**
8346
8489
  * The directory every file matched by `glob` lives under, or `undefined` when
8347
8490
  * the pattern pins down no such directory.
@@ -8374,7 +8517,7 @@ function getGlobStaticPrefix(glob) {
8374
8517
  const lastIndex = segments.length - 1;
8375
8518
  const prefix = [];
8376
8519
  for (const [index, segment] of segments.entries()) {
8377
- if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
8520
+ if (segment === "" || GLOB_METACHARACTERS$1.test(segment)) break;
8378
8521
  if (segment === ".") continue;
8379
8522
  if (index === lastIndex) break;
8380
8523
  prefix.push(segment);
@@ -9838,6 +9981,22 @@ allowedMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })
9838
9981
  injectAgentsMd: zod_mini.z.optional(zod_mini.z.boolean()),
9839
9982
  mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
9840
9983
  })),
9984
+ pool: zod_mini.z.optional(zod_mini.z.looseObject({
9985
+ type: zod_mini.z.optional(zod_mini.z.enum([
9986
+ "in_process",
9987
+ "command",
9988
+ "agent_server"
9989
+ ])),
9990
+ description: zod_mini.z.optional(zod_mini.z.string()),
9991
+ instructions: zod_mini.z.optional(zod_mini.z.string()),
9992
+ command: zod_mini.z.optional(zod_mini.z.string()),
9993
+ args: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
9994
+ env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
9995
+ agent_server: zod_mini.z.optional(zod_mini.z.string()),
9996
+ session_config_options: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.unknown())),
9997
+ inherit_agent_config: zod_mini.z.optional(zod_mini.z.boolean()),
9998
+ disabled: zod_mini.z.optional(zod_mini.z.boolean())
9999
+ })),
9841
10000
  vibe: zod_mini.z.optional(zod_mini.z.looseObject({
9842
10001
  agent_type: zod_mini.z.optional(zod_mini.z.enum(["agent", "subagent"])),
9843
10002
  display_name: zod_mini.z.optional(zod_mini.z.string()),
@@ -10425,6 +10584,17 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
10425
10584
  this.logger = logger;
10426
10585
  }
10427
10586
  /**
10587
+ * Whether `convertRulesyncFilesToToolFiles` still yields tool files for an
10588
+ * empty source list. A processor that aggregates into a shared file it may
10589
+ * never delete (Pool's `settings.yaml` subagents) answers true so a run
10590
+ * whose last source file is gone retracts what an earlier run generated;
10591
+ * the default keeps a run with no source from writing anything. Consulted
10592
+ * after `loadRulesyncFiles`, so the answer may depend on what that load saw.
10593
+ */
10594
+ emitsToolFilesForEmptySource() {
10595
+ return false;
10596
+ }
10597
+ /**
10428
10598
  * Return tool targets that this feature supports.
10429
10599
  */
10430
10600
  static getToolTargets(_params = {}) {
@@ -10439,6 +10609,11 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
10439
10609
  const changedPaths = [];
10440
10610
  for (const aiFile of aiFiles) {
10441
10611
  const filePath = aiFile.getFilePath();
10612
+ if (await refusesWriteOutsideRoot({
10613
+ logger: this.logger,
10614
+ rootPath: aiFile.getOutputRoot(),
10615
+ targetPath: filePath
10616
+ })) continue;
10442
10617
  const existingFileContent = await readFileContentOrNull(filePath);
10443
10618
  if (existingFileContent !== null && aiFile.shouldMergeExistingFileContent()) aiFile.setFileContent(existingFileContent);
10444
10619
  const contentWithNewline = addTrailingNewline(aiFile.getFileContent());
@@ -10571,6 +10746,37 @@ function mergeByIdentity({ perRoot, identity }) {
10571
10746
  return order.map((key) => winnerByKey.get(key));
10572
10747
  }
10573
10748
  /**
10749
+ * Whether a write to `targetPath` has to be refused because a link on the way
10750
+ * there leads out of `rootPath`, warning about the refusal.
10751
+ *
10752
+ * A checked-out repository can carry a symbolic link at an output path
10753
+ * (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`), and a write
10754
+ * through it would land wherever the link points. Every writer that lets the
10755
+ * OS follow links — file, directory, and config-rewrite alike — asks this first
10756
+ * so that one warning describes the one behavior.
10757
+ */
10758
+ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
10759
+ if (!await writablePathEscapesRoot({
10760
+ rootPath,
10761
+ targetPath
10762
+ })) return false;
10763
+ logger.warn(`Refusing to write ${quoteForLog(targetPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
10764
+ return true;
10765
+ }
10766
+ /**
10767
+ * {@link refusesWriteOutsideRoot} over every path a single unit of output
10768
+ * writes, stopping at the first refusal: a directory written as one unit is
10769
+ * held back whole when any one file in it would land outside the root.
10770
+ */
10771
+ async function refusesAnyWriteOutsideRoot({ logger, rootPath, targetPaths }) {
10772
+ for (const targetPath of targetPaths) if (await refusesWriteOutsideRoot({
10773
+ logger,
10774
+ rootPath,
10775
+ targetPath
10776
+ })) return true;
10777
+ return false;
10778
+ }
10779
+ /**
10574
10780
  * The key two spellings share when a case-insensitive filesystem would give
10575
10781
  * them one file. `toLowerCase()` is locale-independent (unlike
10576
10782
  * `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
@@ -12949,6 +13155,8 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
12949
13155
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
12950
13156
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
12951
13157
  const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
13158
+ const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
13159
+ const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
12952
13160
  /**
12953
13161
  * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
12954
13162
  * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
@@ -13030,6 +13238,38 @@ const KIMI_CODE_CONFIG_DECLARATION = {
13030
13238
  }
13031
13239
  };
13032
13240
  /**
13241
+ * Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
13242
+ * the user's primary Pool settings file, so an unparseable root fails closed
13243
+ * rather than being replaced with generated output. `mcp_servers` is owned as
13244
+ * a whole key because the MCP writer emits the complete server map. The
13245
+ * permissions writer deep-merges: it rewrites the `allow`/`deny` lists of the
13246
+ * `tools.<name>` blocks it manages and the `paths` lists, leaving each tool's
13247
+ * `disabled` flag, unmanaged tools and every other key in place. `hooks` is
13248
+ * owned as a whole key because the hooks writer emits every event list.
13249
+ * `subagents` is owned as a whole key too: its writer recomputes the block
13250
+ * from the existing file (`default` and the built-in `general` agent carried
13251
+ * over, every other agent replaced) before applying the patch.
13252
+ */
13253
+ const POOL_SETTINGS_DECLARATION = {
13254
+ format: "yaml",
13255
+ invalidRootPolicy: "error",
13256
+ features: {
13257
+ mcp: {
13258
+ kind: "replace-owned-keys",
13259
+ ownedKeys: ["mcp_servers"]
13260
+ },
13261
+ permissions: { kind: "deep-merge" },
13262
+ hooks: {
13263
+ kind: "replace-owned-keys",
13264
+ ownedKeys: ["hooks"]
13265
+ },
13266
+ subagents: {
13267
+ kind: "replace-owned-keys",
13268
+ ownedKeys: ["subagents"]
13269
+ }
13270
+ }
13271
+ };
13272
+ /**
13033
13273
  * ZCode's settings file, which also carries model/theme/permission keys
13034
13274
  * rulesync does not own. Both copies are the user's primary ZCode config, so
13035
13275
  * every writer refuses to read-modify-write a file it could not parse rather
@@ -13134,6 +13374,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13134
13374
  [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13135
13375
  [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13136
13376
  [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
13377
+ [POOL_PROJECT_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
13378
+ [POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
13137
13379
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
13138
13380
  format: "yaml",
13139
13381
  invalidRootPolicy: "error",
@@ -20420,6 +20662,11 @@ var CommandsProcessor = class extends FeatureProcessor {
20420
20662
  global: this.global,
20421
20663
  relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
20422
20664
  }));
20665
+ if (await refusesWriteOutsideRoot({
20666
+ logger: this.logger,
20667
+ rootPath: this.outputRoot,
20668
+ targetPath: configPath
20669
+ })) return changedCount;
20423
20670
  const currentContent = await readFileContentOrNull(configPath);
20424
20671
  if (currentContent === null) return changedCount;
20425
20672
  const nextContent = getDisabledHermesCommandsPluginConfigContent({
@@ -20442,6 +20689,11 @@ var CommandsProcessor = class extends FeatureProcessor {
20442
20689
  if (this.toolTarget !== "goose" || !this.global) return 0;
20443
20690
  if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
20444
20691
  const configPath = (0, node_path.join)(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
20692
+ if (await refusesWriteOutsideRoot({
20693
+ logger: this.logger,
20694
+ rootPath: this.outputRoot,
20695
+ targetPath: configPath
20696
+ })) return 0;
20445
20697
  const currentContent = await readFileContentOrNull(configPath);
20446
20698
  if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
20447
20699
  const nextContent = getGooseSlashCommandsConfigContent({
@@ -27541,6 +27793,302 @@ var PiHooks = class PiHooks extends ToolHooks {
27541
27793
  }
27542
27794
  };
27543
27795
  //#endregion
27796
+ //#region src/constants/pool-paths.ts
27797
+ /**
27798
+ * Pool (Poolside's coding agent CLI) configuration-layout conventions.
27799
+ *
27800
+ * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
27801
+ * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
27802
+ * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
27803
+ * path), the project-root `AGENTS.md`, and nested per-directory
27804
+ * `AGENTS.md` files from the repository root down through the working
27805
+ * directory, deeper files taking precedence. It skips ignored directories
27806
+ * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
27807
+ *
27808
+ * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
27809
+ * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
27810
+ * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
27811
+ * the skill directories of other Agent Skills tools; rulesync writes only the
27812
+ * Pool-specific roots and leaves the shared ones to their own targets.
27813
+ *
27814
+ * @see https://docs.poolside.ai/agent-instructions
27815
+ * @see https://docs.poolside.ai/skills
27816
+ * @see https://github.com/poolsideai/pool
27817
+ */
27818
+ /** Project-scoped `.poolside/` directory at the project root. */
27819
+ const POOL_DIR = ".poolside";
27820
+ /** Global config directory for Pool, relative to the home directory. */
27821
+ const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
27822
+ /** Project skills root, relative to the project root. */
27823
+ const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
27824
+ /** Global skills root, relative to the home directory. */
27825
+ const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
27826
+ /**
27827
+ * Pool's settings file name. MCP servers live under the top-level
27828
+ * `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
27829
+ * `~/.config/poolside/settings.yaml` (global); the untracked
27830
+ * `.poolside/settings.local.yaml` overlay is left to the user.
27831
+ *
27832
+ * @see https://docs.poolside.ai/mcp-servers
27833
+ * @see https://docs.poolside.ai/settings-file-reference
27834
+ */
27835
+ const POOL_SETTINGS_FILE_NAME = "settings.yaml";
27836
+ /** Pool's untracked per-user overlay next to the settings file; never written. */
27837
+ const POOL_SETTINGS_LOCAL_FILE_NAME = "settings.local.yaml";
27838
+ /** Top-level key of Pool's settings file that holds the MCP server map. */
27839
+ const POOL_MCP_SERVERS_KEY = "mcp_servers";
27840
+ /** Top-level key of Pool's settings file that holds per-tool permission rules. */
27841
+ const POOL_TOOLS_KEY = "tools";
27842
+ /** Top-level key of Pool's settings file that holds file access rules. */
27843
+ const POOL_PATHS_KEY = "paths";
27844
+ /** Top-level key of Pool's settings file that holds the per-event hook lists. */
27845
+ const POOL_HOOKS_KEY = "hooks";
27846
+ /**
27847
+ * Top-level key of Pool's settings file that holds the subagent registry:
27848
+ * `subagents.default` names the agent delegated to by default and
27849
+ * `subagents.agents.<name>` defines each agent.
27850
+ *
27851
+ * @see https://docs.poolside.ai/subagents
27852
+ */
27853
+ const POOL_SUBAGENTS_KEY = "subagents";
27854
+ /** Key under `subagents` that holds the per-agent definitions. */
27855
+ const POOL_SUBAGENTS_AGENTS_KEY = "agents";
27856
+ /** Pool's reserved built-in agent, always `in_process` and never generated. */
27857
+ const POOL_GENERAL_AGENT_NAME = "general";
27858
+ //#endregion
27859
+ //#region src/features/hooks/pool-hooks.ts
27860
+ /** The matcher Pool reads as "any tool"; `""` means the same. */
27861
+ const POOL_MATCH_ALL = "*";
27862
+ const SUPPORTED_POOL_EVENTS = new Set(POOL_HOOK_EVENTS);
27863
+ /**
27864
+ * The Pool events that test `matcher` against a tool name. Pool ignores the
27865
+ * field on every other event (it stays required by the schema), so an
27866
+ * authored matcher there is dropped with a warning and `*` written instead.
27867
+ */
27868
+ const POOL_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
27869
+ /**
27870
+ * Single spelling of the settings.yaml codec/policy, matching PoolMcp and
27871
+ * PoolPermissions: fail closed on an unparseable root rather than replacing
27872
+ * the user's primary Pool settings with generated output.
27873
+ */
27874
+ function parsePoolSettings$3(fileContent, filePath) {
27875
+ return parseSharedConfig({
27876
+ format: "yaml",
27877
+ fileContent,
27878
+ filePath,
27879
+ invalidRootPolicy: "error"
27880
+ });
27881
+ }
27882
+ /**
27883
+ * Build the `hooks` event lists of Pool's settings file from a canonical hooks
27884
+ * config. Pool keys a flat list of `{name, matcher, command, timeout}` entries
27885
+ * by event name. Only `type: "command"` canonical hooks are emitted, since a
27886
+ * Pool hook is a shell command. A shared canonical event Pool does not fire is
27887
+ * skipped (the HooksProcessor reports it), while an event under the
27888
+ * `pool.hooks` override — such as one an import filed there — is written
27889
+ * verbatim so it round-trips.
27890
+ */
27891
+ function canonicalToPoolHooks({ config, toolOverride, logger }) {
27892
+ const sharedHooks = {};
27893
+ for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_POOL_EVENTS.has(event)) sharedHooks[event] = defs;
27894
+ const effective = {
27895
+ ...sharedHooks,
27896
+ ...toolOverride
27897
+ };
27898
+ const hooks = {};
27899
+ for (const [event, defs] of Object.entries(effective)) {
27900
+ if (isPrototypePollutionKey(event)) continue;
27901
+ const poolEvent = lookupOwn({
27902
+ record: CANONICAL_TO_POOL_EVENT_NAMES,
27903
+ key: event
27904
+ }) ?? event;
27905
+ const entries = [];
27906
+ for (const def of defs) {
27907
+ const entry = canonicalDefToPoolEntry({
27908
+ def,
27909
+ event,
27910
+ poolEvent,
27911
+ logger
27912
+ });
27913
+ if (entry !== null) entries.push(entry);
27914
+ }
27915
+ if (entries.length > 0) hooks[poolEvent] = entries;
27916
+ }
27917
+ return hooks;
27918
+ }
27919
+ /** Convert one canonical hook definition to a Pool entry, or null to skip. */
27920
+ function canonicalDefToPoolEntry({ def, event, poolEvent, logger }) {
27921
+ if ((def.type ?? "command") !== "command") return null;
27922
+ if (typeof def.command !== "string" || def.command === "") {
27923
+ logger?.warn(`Pool hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Pool would reject, so it was skipped.`);
27924
+ return null;
27925
+ }
27926
+ const authoredMatcher = typeof def.matcher === "string" && def.matcher !== "" ? def.matcher : POOL_MATCH_ALL;
27927
+ let matcher = authoredMatcher;
27928
+ if (!POOL_MATCHER_EVENTS.has(poolEvent) && authoredMatcher !== POOL_MATCH_ALL) {
27929
+ logger?.warn(`Pool ignores "matcher" on "${poolEvent}", so the matcher ${quoteValueForWarning(authoredMatcher)} of the hook ${quoteValueForWarning(def.name ?? def.command)} under "${event}" was dropped.`);
27930
+ matcher = POOL_MATCH_ALL;
27931
+ }
27932
+ const entry = {
27933
+ ...typeof def.name === "string" && def.name !== "" ? { name: def.name } : {},
27934
+ matcher,
27935
+ command: def.command
27936
+ };
27937
+ if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
27938
+ return entry;
27939
+ }
27940
+ /** Convert one raw hook entry to a canonical definition, or null to skip. */
27941
+ function poolEntryToCanonicalDef({ raw, poolEvent }) {
27942
+ if (!isRecord$1(raw) || typeof raw.command !== "string" || raw.command === "") return null;
27943
+ const def = {
27944
+ type: "command",
27945
+ command: raw.command
27946
+ };
27947
+ if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
27948
+ if (POOL_MATCHER_EVENTS.has(poolEvent) && typeof raw.matcher === "string" && raw.matcher !== "" && raw.matcher !== POOL_MATCH_ALL) def.matcher = raw.matcher;
27949
+ if (typeof raw.timeout === "number") def.timeout = raw.timeout;
27950
+ return def;
27951
+ }
27952
+ /**
27953
+ * Reverse {@link canonicalToPoolHooks}: parse the event lists of the `hooks`
27954
+ * block back into a canonical event → definition[] record. An event Pool does
27955
+ * not document is carried under its own name so `buildImportedHooksConfig`
27956
+ * files it under the `pool.hooks` override; a non-list sibling such as
27957
+ * `stop_hook_max_continuations` is not a hook and is left alone.
27958
+ */
27959
+ function poolHooksToCanonical(hooksBlock) {
27960
+ const canonical = {};
27961
+ if (!isRecord$1(hooksBlock)) return canonical;
27962
+ for (const [poolEvent, rawEntries] of Object.entries(hooksBlock)) {
27963
+ if (isPrototypePollutionKey(poolEvent) || !Array.isArray(rawEntries)) continue;
27964
+ const canonicalEvent = lookupOwn({
27965
+ record: POOL_TO_CANONICAL_EVENT_NAMES,
27966
+ key: poolEvent
27967
+ }) ?? poolEvent;
27968
+ const defs = rawEntries.map((raw) => poolEntryToCanonicalDef({
27969
+ raw,
27970
+ poolEvent
27971
+ })).filter((def) => def !== null);
27972
+ if (defs.length === 0) continue;
27973
+ canonical[canonicalEvent] = [...lookupOwn({
27974
+ record: canonical,
27975
+ key: canonicalEvent
27976
+ }) ?? [], ...defs];
27977
+ }
27978
+ return canonical;
27979
+ }
27980
+ /**
27981
+ * The keys of an existing `hooks` block that are not event lists —
27982
+ * `stop_hook_max_continuations` and anything else Pool may add beside the
27983
+ * events — which survive a regenerate untouched.
27984
+ */
27985
+ function nonEventHookKeys(hooksBlock) {
27986
+ const kept = {};
27987
+ if (!isRecord$1(hooksBlock)) return kept;
27988
+ for (const [key, value] of Object.entries(hooksBlock)) if (!isPrototypePollutionKey(key) && !Array.isArray(value)) kept[key] = value;
27989
+ return kept;
27990
+ }
27991
+ /**
27992
+ * Pool hooks.
27993
+ *
27994
+ * Pool reads hooks from the `hooks` key of its settings file —
27995
+ * `.poolside/settings.yaml` at project scope and
27996
+ * `~/.config/poolside/settings.yaml` at user scope — as
27997
+ * `hooks.<Event>: [{name?, matcher, command, timeout?}]` for `PreToolUse`,
27998
+ * `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `SessionStart` and `Stop`.
27999
+ * The event lists are rewritten as a whole; the block's non-list sibling
28000
+ * `stop_hook_max_continuations` and every other top-level key of the file
28001
+ * (`mcp_servers`, `tools`, `paths`, `pool`, ...) are preserved, and the file
28002
+ * is never deleted.
28003
+ *
28004
+ * @see https://docs.poolside.ai/hooks
28005
+ * @see https://docs.poolside.ai/settings-file-reference
28006
+ */
28007
+ var PoolHooks = class PoolHooks extends ToolHooks {
28008
+ settings;
28009
+ constructor(params) {
28010
+ super(params);
28011
+ this.settings = parsePoolSettings$3(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
28012
+ }
28013
+ getSettings() {
28014
+ return this.settings;
28015
+ }
28016
+ isDeletable() {
28017
+ return false;
28018
+ }
28019
+ static getSettablePaths({ global = false } = {}) {
28020
+ return {
28021
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
28022
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
28023
+ };
28024
+ }
28025
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
28026
+ const paths = this.getSettablePaths({ global });
28027
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
28028
+ return new PoolHooks({
28029
+ outputRoot,
28030
+ relativeDirPath: paths.relativeDirPath,
28031
+ relativeFilePath: paths.relativeFilePath,
28032
+ fileContent,
28033
+ validate,
28034
+ global
28035
+ });
28036
+ }
28037
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
28038
+ const paths = this.getSettablePaths({ global });
28039
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
28040
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
28041
+ const existing = parsePoolSettings$3(existingContent, filePath);
28042
+ const config = rulesyncHooks.getJson();
28043
+ const hooks = canonicalToPoolHooks({
28044
+ config,
28045
+ toolOverride: config.pool?.hooks,
28046
+ logger
28047
+ });
28048
+ return new PoolHooks({
28049
+ outputRoot,
28050
+ relativeDirPath: paths.relativeDirPath,
28051
+ relativeFilePath: paths.relativeFilePath,
28052
+ fileContent: applySharedConfigPatch({
28053
+ fileKey: sharedConfigFileKey(paths),
28054
+ feature: "hooks",
28055
+ existingContent,
28056
+ patch: { [POOL_HOOKS_KEY]: {
28057
+ ...nonEventHookKeys(existing[POOL_HOOKS_KEY]),
28058
+ ...hooks
28059
+ } },
28060
+ filePath,
28061
+ logger
28062
+ }),
28063
+ validate,
28064
+ global
28065
+ });
28066
+ }
28067
+ toRulesyncHooks() {
28068
+ const hooks = poolHooksToCanonical(this.settings[POOL_HOOKS_KEY]);
28069
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
28070
+ hooks,
28071
+ overrideKey: "pool"
28072
+ }), null, 2) });
28073
+ }
28074
+ validate() {
28075
+ return {
28076
+ success: true,
28077
+ error: null
28078
+ };
28079
+ }
28080
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
28081
+ return new PoolHooks({
28082
+ outputRoot,
28083
+ relativeDirPath,
28084
+ relativeFilePath,
28085
+ fileContent: "",
28086
+ validate: false,
28087
+ global
28088
+ });
28089
+ }
28090
+ };
28091
+ //#endregion
27544
28092
  //#region src/features/hooks/qwencode-hooks.ts
27545
28093
  /**
27546
28094
  * Build a single Qwen Code hook object from a canonical hook definition.
@@ -28654,14 +29202,15 @@ const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetT
28654
29202
  /**
28655
29203
  * Event names present in the config that the target's adapter cannot emit.
28656
29204
  *
28657
- * When the factory passes override-block keys through verbatim
28658
- * (`passthroughOverrideEvents`), those keys are excluded from the check so
28659
- * documented passthrough triggers aren't falsely reported as skipped.
29205
+ * Shared (`hooks`) keys are always checked. Override-block keys are only
29206
+ * checked when the factory declares `dropsUnknownOverrideEvents`; otherwise
29207
+ * the adapter either emits them verbatim or reports its own drops, and a
29208
+ * generic "not supported" warning would be wrong or duplicated.
28660
29209
  */
28661
29210
  function unsupportedEventNames(params) {
28662
29211
  const { factory, sharedHooks, effectiveHooks } = params;
28663
29212
  const supportedEvents = new Set(factory.supportedEvents);
28664
- const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
29213
+ const eventNames = factory.dropsUnknownOverrideEvents ? Object.keys(effectiveHooks) : Object.keys(sharedHooks);
28665
29214
  return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
28666
29215
  }
28667
29216
  /**
@@ -28754,7 +29303,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28754
29303
  supportedEvents: AMP_HOOK_EVENTS,
28755
29304
  supportedHookTypes: ["command"],
28756
29305
  supportsMatcher: true,
28757
- matcherEvents: ["preToolUse", "postToolUse"]
29306
+ matcherEvents: ["preToolUse", "postToolUse"],
29307
+ dropsUnknownOverrideEvents: true
28758
29308
  }],
28759
29309
  ["antigravity-cli", {
28760
29310
  class: AntigravityCliHooks,
@@ -28787,7 +29337,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28787
29337
  },
28788
29338
  supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
28789
29339
  supportedHookTypes: ["command"],
28790
- supportsMatcher: true
29340
+ supportsMatcher: true,
29341
+ dropsUnknownOverrideEvents: true
28791
29342
  }],
28792
29343
  ["cursor", {
28793
29344
  class: CursorHooks,
@@ -28832,7 +29383,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28832
29383
  "mcp_tool",
28833
29384
  "agent"
28834
29385
  ],
28835
- supportsMatcher: true
29386
+ supportsMatcher: true,
29387
+ dropsUnknownOverrideEvents: true
28836
29388
  }],
28837
29389
  ["codexcli", {
28838
29390
  class: CodexcliHooks,
@@ -28893,7 +29445,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28893
29445
  supportedEvents: KILO_HOOK_EVENTS,
28894
29446
  supportedHookTypes: ["command"],
28895
29447
  supportsMatcher: true,
28896
- matcherEvents: ["preToolUse", "postToolUse"]
29448
+ matcherEvents: ["preToolUse", "postToolUse"],
29449
+ dropsUnknownOverrideEvents: true
28897
29450
  }],
28898
29451
  ["opencode", {
28899
29452
  class: OpencodeHooks,
@@ -28905,7 +29458,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28905
29458
  supportedEvents: OPENCODE_HOOK_EVENTS,
28906
29459
  supportedHookTypes: ["command"],
28907
29460
  supportsMatcher: true,
28908
- matcherEvents: ["preToolUse", "postToolUse"]
29461
+ matcherEvents: ["preToolUse", "postToolUse"],
29462
+ dropsUnknownOverrideEvents: true
28909
29463
  }],
28910
29464
  ["pi", {
28911
29465
  class: PiHooks,
@@ -28916,7 +29470,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28916
29470
  },
28917
29471
  supportedEvents: PI_HOOK_EVENTS,
28918
29472
  supportedHookTypes: ["command"],
28919
- supportsMatcher: true
29473
+ supportsMatcher: true,
29474
+ dropsUnknownOverrideEvents: true
28920
29475
  }],
28921
29476
  ["factorydroid", {
28922
29477
  class: FactorydroidHooks,
@@ -28938,7 +29493,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28938
29493
  },
28939
29494
  supportedEvents: CLINE_HOOK_EVENTS,
28940
29495
  supportedHookTypes: ["command"],
28941
- supportsMatcher: false
29496
+ supportsMatcher: false,
29497
+ dropsUnknownOverrideEvents: true
28942
29498
  }],
28943
29499
  ["goose", {
28944
29500
  class: GooseHooks,
@@ -28960,8 +29516,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28960
29516
  },
28961
29517
  supportedEvents: HERMESAGENT_HOOK_EVENTS,
28962
29518
  supportedHookTypes: ["command"],
28963
- supportsMatcher: true,
28964
- passthroughOverrideEvents: true
29519
+ supportsMatcher: true
28965
29520
  }],
28966
29521
  ["kimi-code", {
28967
29522
  class: KimiCodeHooks,
@@ -28972,8 +29527,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28972
29527
  },
28973
29528
  supportedEvents: KIMI_CODE_HOOK_EVENTS,
28974
29529
  supportedHookTypes: ["command"],
28975
- supportsMatcher: true,
28976
- passthroughOverrideEvents: true
29530
+ supportsMatcher: true
28977
29531
  }],
28978
29532
  ["deepagents", {
28979
29533
  class: DeepagentsHooks,
@@ -28984,7 +29538,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28984
29538
  },
28985
29539
  supportedEvents: DEEPAGENTS_HOOK_EVENTS,
28986
29540
  supportedHookTypes: ["command"],
28987
- supportsMatcher: true
29541
+ supportsMatcher: true,
29542
+ dropsUnknownOverrideEvents: true
28988
29543
  }],
28989
29544
  ["kiro", {
28990
29545
  class: KiroHooks,
@@ -28995,8 +29550,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28995
29550
  },
28996
29551
  supportedEvents: KIRO_HOOK_EVENTS,
28997
29552
  supportedHookTypes: ["command"],
28998
- supportsMatcher: true,
28999
- passthroughOverrideEvents: true
29553
+ supportsMatcher: true
29000
29554
  }],
29001
29555
  ["kiro-cli", {
29002
29556
  class: KiroCliHooks,
@@ -29007,8 +29561,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29007
29561
  },
29008
29562
  supportedEvents: KIRO_IDE_HOOK_EVENTS,
29009
29563
  supportedHookTypes: ["command", "prompt"],
29010
- supportsMatcher: true,
29011
- passthroughOverrideEvents: true
29564
+ supportsMatcher: true
29012
29565
  }],
29013
29566
  ["kiro-ide", {
29014
29567
  class: KiroIdeHooks,
@@ -29019,8 +29572,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29019
29572
  },
29020
29573
  supportedEvents: KIRO_IDE_HOOK_EVENTS,
29021
29574
  supportedHookTypes: ["command", "prompt"],
29022
- supportsMatcher: true,
29023
- passthroughOverrideEvents: true
29575
+ supportsMatcher: true
29024
29576
  }],
29025
29577
  ["devin", {
29026
29578
  class: DevinHooks,
@@ -29138,6 +29690,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29138
29690
  },
29139
29691
  supportedEvents: VIBE_HOOK_EVENTS,
29140
29692
  supportedHookTypes: ["command"],
29693
+ supportsMatcher: true,
29694
+ dropsUnknownOverrideEvents: true
29695
+ }],
29696
+ ["pool", {
29697
+ class: PoolHooks,
29698
+ meta: {
29699
+ supportsProject: true,
29700
+ supportsGlobal: true,
29701
+ supportsImport: true
29702
+ },
29703
+ supportedEvents: POOL_HOOK_EVENTS,
29704
+ supportedHookTypes: ["command"],
29141
29705
  supportsMatcher: true
29142
29706
  }],
29143
29707
  ["qwencode", {
@@ -29165,7 +29729,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29165
29729
  },
29166
29730
  supportedEvents: REASONIX_HOOK_EVENTS,
29167
29731
  supportedHookTypes: ["command"],
29168
- supportsMatcher: true
29732
+ supportsMatcher: true,
29733
+ dropsUnknownOverrideEvents: true
29169
29734
  }],
29170
29735
  ["grokcli", {
29171
29736
  class: GrokcliHooks,
@@ -29187,8 +29752,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29187
29752
  },
29188
29753
  supportedEvents: CRUSH_HOOK_EVENTS,
29189
29754
  supportedHookTypes: ["command"],
29190
- supportsMatcher: true,
29191
- passthroughOverrideEvents: true
29755
+ supportsMatcher: true
29192
29756
  }],
29193
29757
  ["zcode", {
29194
29758
  class: ZcodeHooks,
@@ -37119,6 +37683,320 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
37119
37683
  }
37120
37684
  };
37121
37685
  //#endregion
37686
+ //#region src/features/mcp/pool-mcp.ts
37687
+ /**
37688
+ * Single spelling of the settings.yaml codec/policy, matching the
37689
+ * `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
37690
+ * unparseable root rather than replacing the user's primary Pool settings with
37691
+ * generated output.
37692
+ */
37693
+ function parsePoolSettings$2(fileContent, filePath) {
37694
+ return parseSharedConfig({
37695
+ format: "yaml",
37696
+ fileContent,
37697
+ filePath,
37698
+ invalidRootPolicy: "error"
37699
+ });
37700
+ }
37701
+ /**
37702
+ * The remote transports Pool's `transport.type` accepts. `streamable-http` is
37703
+ * the canonical rulesync name for what Pool calls `http`; everything else is
37704
+ * left to the caller to reject.
37705
+ *
37706
+ * @see https://docs.poolside.ai/mcp-servers
37707
+ */
37708
+ function asPoolRemoteType(stated, url) {
37709
+ if (stated === "sse") return "sse";
37710
+ if (stated === "http" || stated === "streamable-http") return "http";
37711
+ if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
37712
+ }
37713
+ /**
37714
+ * Pool spells remote headers as a list of `"Name: value"` strings rather than
37715
+ * a map, so the canonical record is flattened at generate time and split back
37716
+ * at the first `:` on import. A list entry without a `:` names no header and
37717
+ * is dropped with a warning — silently losing what may be a mistyped auth
37718
+ * header would leave the canonical file short of one without a trace.
37719
+ */
37720
+ function headersRecordToPoolList(headers) {
37721
+ return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
37722
+ }
37723
+ function poolHeadersListToRecord(serverName, headers) {
37724
+ if (!isStringArray$2(headers)) return void 0;
37725
+ const result = {};
37726
+ for (const entry of headers) {
37727
+ const separator = entry.indexOf(":");
37728
+ const name = separator > 0 ? entry.slice(0, separator).trim() : "";
37729
+ if (name === "" || PROTOTYPE_POLLUTION_KEYS.has(name)) {
37730
+ warnWithFallback(void 0, `Ignored malformed header ${quoteValueForWarning(entry)} in Pool MCP server ${quoteValueForWarning(serverName)}: expected "Name: value"`);
37731
+ continue;
37732
+ }
37733
+ result[name] = entry.slice(separator + 1).trim();
37734
+ }
37735
+ return Object.keys(result).length > 0 ? result : void 0;
37736
+ }
37737
+ /**
37738
+ * Pool's `deny`/`enabled_tools` entries are glob patterns, while the canonical
37739
+ * `enabledTools`/`disabledTools` lists are literal tool names that the other
37740
+ * targets match verbatim. A pattern therefore imports as a name that matches
37741
+ * nothing elsewhere — a `deny` that quietly stops denying is the case worth
37742
+ * calling out — so it is copied as-is but warned about. `allow` is exempt: it
37743
+ * imports to the Pool-only `poolAllow`, where globs are exactly what Pool
37744
+ * expects and no other tool ever reads them.
37745
+ */
37746
+ const GLOB_METACHARACTERS = /[*?[]/;
37747
+ /** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
37748
+ const MAX_LISTED_GLOB_PATTERNS = 10;
37749
+ function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
37750
+ if (!isStringArray$2(value)) {
37751
+ warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
37752
+ return;
37753
+ }
37754
+ if (!warnOnGlobs) return value;
37755
+ const patterns = value.filter((entry) => GLOB_METACHARACTERS.test(entry));
37756
+ if (patterns.length > 0) {
37757
+ const listed = patterns.slice(0, MAX_LISTED_GLOB_PATTERNS).map(quoteValueForWarning);
37758
+ const overflow = patterns.length - listed.length;
37759
+ const shown = overflow > 0 ? `${listed.join(", ")} (+${overflow} more)` : listed.join(", ");
37760
+ warnWithFallback(void 0, `${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)} contains glob patterns (${shown}); other tools match the imported tool names literally`);
37761
+ }
37762
+ return value;
37763
+ }
37764
+ /**
37765
+ * Pool's `transport` block for a remote server, or `null` (after a warning)
37766
+ * when the server has no url or names a transport Pool cannot reach.
37767
+ */
37768
+ function toPoolTransport(name, config, logger) {
37769
+ const url = resolveRemoteMcpUrl(config);
37770
+ if (!url) return warnAndSkipMcpServer({
37771
+ toolName: "Pool",
37772
+ serverName: name,
37773
+ reason: "a remote transport without a url",
37774
+ logger
37775
+ });
37776
+ const stated = config.type ?? config.transport;
37777
+ const type = asPoolRemoteType(stated, url);
37778
+ if (type === void 0) return warnAndSkipMcpServer({
37779
+ toolName: "Pool",
37780
+ serverName: name,
37781
+ 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)`,
37782
+ logger
37783
+ });
37784
+ const transport = {
37785
+ type,
37786
+ url
37787
+ };
37788
+ if (config.headers && Object.keys(config.headers).length > 0) transport.headers = headersRecordToPoolList(config.headers);
37789
+ return transport;
37790
+ }
37791
+ /**
37792
+ * The transport-specific part of a Pool server entry: a `transport` block for
37793
+ * remote servers, `command`/`args`/`cwd` for stdio ones. `null` (after a
37794
+ * warning) when the server cannot be written.
37795
+ */
37796
+ function toPoolTransportFields(name, config, logger) {
37797
+ if (isRemoteMcpServer(config)) {
37798
+ const transport = toPoolTransport(name, config, logger);
37799
+ return transport === null ? null : { transport };
37800
+ }
37801
+ const [command, ...args] = resolveLocalMcpCommand(config);
37802
+ if (!command) return warnAndSkipMcpServer({
37803
+ toolName: "Pool",
37804
+ serverName: name,
37805
+ reason: "a stdio transport without a command",
37806
+ logger
37807
+ });
37808
+ const fields = {
37809
+ command,
37810
+ args
37811
+ };
37812
+ if (config.cwd) fields.cwd = config.cwd;
37813
+ return fields;
37814
+ }
37815
+ /**
37816
+ * The fields shared by stdio and remote entries. Tool filters map onto Pool's
37817
+ * own switches — `enabledTools` becomes `enabled_tools` (the tools Pool
37818
+ * exposes at all) and `disabledTools` becomes `deny` (patterns agents may
37819
+ * never use); the Pool-only approval allowlist is authored as `poolAllow` and
37820
+ * written as `allow`. `disabled: true` is spelled the same on both sides.
37821
+ */
37822
+ function toPoolCommonFields(config) {
37823
+ const fields = {};
37824
+ if (config.env && Object.keys(config.env).length > 0) fields.env = config.env;
37825
+ if (config.enabledTools && config.enabledTools.length > 0) fields.enabled_tools = config.enabledTools;
37826
+ if (config.poolAllow && config.poolAllow.length > 0) fields.allow = config.poolAllow;
37827
+ if (config.disabledTools && config.disabledTools.length > 0) fields.deny = config.disabledTools;
37828
+ if (config.disabled === true) fields.disabled = true;
37829
+ return fields;
37830
+ }
37831
+ /**
37832
+ * Convert canonical rulesync servers to Pool's native `mcp_servers` shape:
37833
+ * stdio servers carry `command`/`args`/`cwd`/`env`, remote servers carry a
37834
+ * `transport` block (`type` of `http` or `sse`, `url`, optional `headers`
37835
+ * list) plus optional `env`; see `toPoolCommonFields` for the tool filters.
37836
+ *
37837
+ * @see https://docs.poolside.ai/mcp-servers
37838
+ */
37839
+ function convertToPoolFormat(mcpServers, logger) {
37840
+ const result = {};
37841
+ for (const [name, config] of Object.entries(mcpServers)) {
37842
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
37843
+ if (!isRecord$1(config)) continue;
37844
+ if (declaresNoTransport(config)) {
37845
+ warnAndSkipMcpServer({
37846
+ toolName: "Pool",
37847
+ serverName: name,
37848
+ reason: "no transport",
37849
+ logger
37850
+ });
37851
+ continue;
37852
+ }
37853
+ const transportFields = toPoolTransportFields(name, config, logger);
37854
+ if (transportFields === null) continue;
37855
+ result[name] = {
37856
+ ...transportFields,
37857
+ ...toPoolCommonFields(config)
37858
+ };
37859
+ }
37860
+ return result;
37861
+ }
37862
+ const POOL_TO_CANONICAL_TOOL_LIST_KEYS = {
37863
+ enabled_tools: "enabledTools",
37864
+ deny: "disabledTools",
37865
+ allow: "poolAllow"
37866
+ };
37867
+ /**
37868
+ * Convert Pool's native `mcp_servers` shape back to canonical rulesync
37869
+ * servers: the `transport` block is flattened to `type`/`url`/`headers`,
37870
+ * `enabled_tools` maps back to `enabledTools`, `deny` to `disabledTools` and
37871
+ * `allow` to the namespaced `poolAllow` (see `McpServerSchema`). Unknown keys
37872
+ * pass through untouched so an import keeps whatever a hand-authored entry
37873
+ * declared; generation back out is a whitelist, so only the keys
37874
+ * `convertToPoolFormat` writes are re-emitted.
37875
+ */
37876
+ function convertFromPoolFormat(poolServers) {
37877
+ const result = {};
37878
+ for (const [name, config] of Object.entries(poolServers)) {
37879
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
37880
+ const converted = {};
37881
+ for (const [key, value] of Object.entries(config)) {
37882
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
37883
+ if (key === "transport") {
37884
+ if (!isRecord$1(value)) continue;
37885
+ if (typeof value.type === "string") converted.type = value.type;
37886
+ if (typeof value.url === "string") converted.url = value.url;
37887
+ const headers = poolHeadersListToRecord(name, value.headers);
37888
+ if (headers) converted.headers = headers;
37889
+ continue;
37890
+ }
37891
+ const canonicalKey = Object.hasOwn(POOL_TO_CANONICAL_TOOL_LIST_KEYS, key) ? POOL_TO_CANONICAL_TOOL_LIST_KEYS[key] : void 0;
37892
+ if (canonicalKey) {
37893
+ const list = importPoolToolList({
37894
+ serverName: name,
37895
+ poolKey: key,
37896
+ value,
37897
+ warnOnGlobs: canonicalKey !== "poolAllow"
37898
+ });
37899
+ if (list) converted[canonicalKey] = list;
37900
+ continue;
37901
+ }
37902
+ converted[key] = value;
37903
+ }
37904
+ result[name] = converted;
37905
+ }
37906
+ return result;
37907
+ }
37908
+ /**
37909
+ * Pool MCP servers.
37910
+ *
37911
+ * Pool reads MCP servers from the `mcp_servers` block of its settings file:
37912
+ * `<project>/.poolside/settings.yaml` at project scope (shared and committed)
37913
+ * and `~/.config/poolside/settings.yaml` at user scope. The untracked
37914
+ * `.poolside/settings.local.yaml` overlay, which Pool layers on top of both,
37915
+ * is left to the user. Every other top-level settings key is preserved and
37916
+ * the file is never deleted.
37917
+ *
37918
+ * @see https://docs.poolside.ai/mcp-servers
37919
+ * @see https://docs.poolside.ai/settings-file-reference
37920
+ */
37921
+ var PoolMcp = class PoolMcp extends ToolMcp {
37922
+ settings;
37923
+ constructor(params) {
37924
+ super(params);
37925
+ this.settings = parsePoolSettings$2(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
37926
+ }
37927
+ getSettings() {
37928
+ return this.settings;
37929
+ }
37930
+ isDeletable() {
37931
+ return false;
37932
+ }
37933
+ static getSettablePaths({ global = false } = {}) {
37934
+ return {
37935
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
37936
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
37937
+ };
37938
+ }
37939
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
37940
+ const paths = this.getSettablePaths({ global });
37941
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
37942
+ return new PoolMcp({
37943
+ outputRoot,
37944
+ relativeDirPath: paths.relativeDirPath,
37945
+ relativeFilePath: paths.relativeFilePath,
37946
+ fileContent,
37947
+ validate,
37948
+ global
37949
+ });
37950
+ }
37951
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
37952
+ const paths = this.getSettablePaths({ global });
37953
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
37954
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
37955
+ const converted = convertToPoolFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
37956
+ const rawServer = rulesyncMcp.getRawMcpServer(serverName);
37957
+ const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
37958
+ return [serverName, {
37959
+ ...serverConfig,
37960
+ ...isStringArray$2(poolAllow) && { poolAllow }
37961
+ }];
37962
+ })), logger);
37963
+ return new PoolMcp({
37964
+ outputRoot,
37965
+ relativeDirPath: paths.relativeDirPath,
37966
+ relativeFilePath: paths.relativeFilePath,
37967
+ fileContent: applySharedConfigPatch({
37968
+ fileKey: sharedConfigFileKey(paths),
37969
+ feature: "mcp",
37970
+ existingContent,
37971
+ patch: { [POOL_MCP_SERVERS_KEY]: converted },
37972
+ filePath
37973
+ }),
37974
+ validate,
37975
+ global
37976
+ });
37977
+ }
37978
+ toRulesyncMcp() {
37979
+ const converted = convertFromPoolFormat(isRecord$1(this.settings["mcp_servers"]) ? this.settings[POOL_MCP_SERVERS_KEY] : {});
37980
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
37981
+ }
37982
+ validate() {
37983
+ return {
37984
+ success: true,
37985
+ error: null
37986
+ };
37987
+ }
37988
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
37989
+ return new PoolMcp({
37990
+ outputRoot,
37991
+ relativeDirPath,
37992
+ relativeFilePath,
37993
+ fileContent: "",
37994
+ validate: false,
37995
+ global
37996
+ });
37997
+ }
37998
+ };
37999
+ //#endregion
37122
38000
  //#region src/features/mcp/qwencode-mcp.ts
37123
38001
  /**
37124
38002
  * Map rulesync's canonical per-server tool filters to Qwen Code's field names.
@@ -39566,6 +40444,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
39566
40444
  supportsDisabledTools: true
39567
40445
  }
39568
40446
  }],
40447
+ ["pool", {
40448
+ class: PoolMcp,
40449
+ meta: {
40450
+ supportsProject: true,
40451
+ supportsGlobal: true,
40452
+ supportsEnabledTools: true,
40453
+ supportsDisabledTools: true
40454
+ }
40455
+ }],
39569
40456
  ["qwencode", {
39570
40457
  class: QwencodeMcp,
39571
40458
  meta: {
@@ -40879,7 +41766,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
40879
41766
  function toAntigravityCliToolName(canonical) {
40880
41767
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
40881
41768
  }
40882
- function toCanonicalToolName$8(cliName) {
41769
+ function toCanonicalToolName$9(cliName) {
40883
41770
  return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
40884
41771
  }
40885
41772
  /**
@@ -41095,7 +41982,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
41095
41982
  const processEntries = (entries, action) => {
41096
41983
  for (const entry of entries) {
41097
41984
  const { toolName, pattern } = parsePermissionEntry$1(entry);
41098
- const canonical = toCanonicalToolName$8(toolName);
41985
+ const canonical = toCanonicalToolName$9(toolName);
41099
41986
  if (!permission[canonical]) permission[canonical] = {};
41100
41987
  permission[canonical][pattern] = action;
41101
41988
  }
@@ -41374,7 +42261,7 @@ function toLegacyAugmentToolName(augmentName) {
41374
42261
  key: augmentName
41375
42262
  }) ?? augmentName;
41376
42263
  }
41377
- function toCanonicalToolName$7(augmentName) {
42264
+ function toCanonicalToolName$8(augmentName) {
41378
42265
  const legacyName = toLegacyAugmentToolName(augmentName);
41379
42266
  return lookupOwn({
41380
42267
  record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
@@ -41827,7 +42714,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
41827
42714
  const type = entry.permission.type;
41828
42715
  if (!isBasicAugmentType(type)) continue;
41829
42716
  const legacyToolName = toLegacyAugmentToolName(entry.toolName);
41830
- const canonical = toCanonicalToolName$7(entry.toolName);
42717
+ const canonical = toCanonicalToolName$8(entry.toolName);
41831
42718
  if (forbiddenMapKeys.has(canonical)) {
41832
42719
  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.`);
41833
42720
  continue;
@@ -42072,7 +42959,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
42072
42959
  function toClaudeToolName(canonical) {
42073
42960
  return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
42074
42961
  }
42075
- function toCanonicalToolName$6(claudeName) {
42962
+ function toCanonicalToolName$7(claudeName) {
42076
42963
  return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
42077
42964
  }
42078
42965
  /**
@@ -42986,7 +43873,7 @@ function convertClaudeToRulesyncPermissions(params) {
42986
43873
  const processEntries = (entries, action) => {
42987
43874
  for (const entry of entries) {
42988
43875
  const { toolName, pattern } = parseClaudePermissionEntry(entry);
42989
- const canonical = toCanonicalToolName$6(toolName);
43876
+ const canonical = toCanonicalToolName$7(toolName);
42990
43877
  if (!permission[canonical]) permission[canonical] = {};
42991
43878
  permission[canonical][pattern] = action;
42992
43879
  }
@@ -43847,8 +44734,8 @@ function mapBashActionToDecision(action) {
43847
44734
  //#region src/features/permissions/commandcode-permissions.ts
43848
44735
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
43849
44736
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
43850
- const CATCH_ALL_PATTERN$6 = "*";
43851
- const MCP_CANONICAL_PREFIX$5 = "mcp__";
44737
+ const CATCH_ALL_PATTERN$7 = "*";
44738
+ const MCP_CANONICAL_PREFIX$6 = "mcp__";
43852
44739
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
43853
44740
  const CATEGORY_TO_COMMANDCODE_TOOL = {
43854
44741
  bash: "Shell",
@@ -43902,10 +44789,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
43902
44789
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
43903
44790
  */
43904
44791
  function buildCommandcodeRule(category, pattern) {
43905
- const catchAll = pattern === CATCH_ALL_PATTERN$6 || pattern === "";
43906
- if (category.startsWith(MCP_CANONICAL_PREFIX$5)) return catchAll ? category : `${category}(${pattern})`;
43907
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$5}${pattern}`;
43908
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$6 : null;
44792
+ const catchAll = pattern === CATCH_ALL_PATTERN$7 || pattern === "";
44793
+ if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
44794
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
44795
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$7 : null;
43909
44796
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
43910
44797
  if (tool === void 0) return null;
43911
44798
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -43941,7 +44828,7 @@ function splitCommandcodeRule(rule) {
43941
44828
  }
43942
44829
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
43943
44830
  function isCatchAllSpecifier(inner) {
43944
- return inner === "" || inner === CATCH_ALL_PATTERN$6;
44831
+ return inner === "" || inner === CATCH_ALL_PATTERN$7;
43945
44832
  }
43946
44833
  /**
43947
44834
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -43975,34 +44862,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
43975
44862
  if (server.length === 0) return null;
43976
44863
  const everyServer = {
43977
44864
  category: "mcp",
43978
- pattern: CATCH_ALL_PATTERN$6,
44865
+ pattern: CATCH_ALL_PATTERN$7,
43979
44866
  notInAllow: true
43980
44867
  };
43981
- if (tool.startsWith(MCP_CANONICAL_PREFIX$5)) {
43982
- if (server === CATCH_ALL_PATTERN$6) return everyServer;
43983
- if (server.includes(CATCH_ALL_PATTERN$6)) return null;
44868
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
44869
+ if (server === CATCH_ALL_PATTERN$7) return everyServer;
44870
+ if (server.includes(CATCH_ALL_PATTERN$7)) return null;
43984
44871
  return {
43985
- category: rest === "" || rest === CATCH_ALL_PATTERN$6 ? `${MCP_CANONICAL_PREFIX$5}${server}` : `${MCP_CANONICAL_PREFIX$5}${remainder}`,
43986
- pattern: CATCH_ALL_PATTERN$6,
44872
+ category: rest === "" || rest === CATCH_ALL_PATTERN$7 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
44873
+ pattern: CATCH_ALL_PATTERN$7,
43987
44874
  notInAllow: false
43988
44875
  };
43989
44876
  }
43990
- const category = `${MCP_CANONICAL_PREFIX$5}${remainder}`;
43991
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$6);
44877
+ const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
44878
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$7);
43992
44879
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
43993
44880
  category,
43994
44881
  pattern: inner,
43995
44882
  notInAllow: true
43996
44883
  } : null;
43997
- if (remainder === CATCH_ALL_PATTERN$6) return everyServer;
43998
- if (rest === CATCH_ALL_PATTERN$6 && !server.includes(CATCH_ALL_PATTERN$6)) return {
43999
- category: `${MCP_CANONICAL_PREFIX$5}${server}`,
44000
- pattern: CATCH_ALL_PATTERN$6,
44884
+ if (remainder === CATCH_ALL_PATTERN$7) return everyServer;
44885
+ if (rest === CATCH_ALL_PATTERN$7 && !server.includes(CATCH_ALL_PATTERN$7)) return {
44886
+ category: `${MCP_CANONICAL_PREFIX$6}${server}`,
44887
+ pattern: CATCH_ALL_PATTERN$7,
44001
44888
  notInAllow: true
44002
44889
  };
44003
44890
  return namesTool ? {
44004
44891
  category,
44005
- pattern: CATCH_ALL_PATTERN$6,
44892
+ pattern: CATCH_ALL_PATTERN$7,
44006
44893
  notInAllow: false
44007
44894
  } : null;
44008
44895
  }
@@ -44026,14 +44913,14 @@ function friendlyToolRule({ category, inner }) {
44026
44913
  if (category === "bash") {
44027
44914
  if (isCatchAllSpecifier(inner)) return {
44028
44915
  category,
44029
- pattern: CATCH_ALL_PATTERN$6,
44916
+ pattern: CATCH_ALL_PATTERN$7,
44030
44917
  notInAllow: false
44031
44918
  };
44032
44919
  const normalized = inner.trim().replace(/\s+/g, " ");
44033
44920
  if (normalized === "" || normalized === ":*") return null;
44034
44921
  return isCatchAllSpecifier(normalized) ? {
44035
44922
  category,
44036
- pattern: CATCH_ALL_PATTERN$6,
44923
+ pattern: CATCH_ALL_PATTERN$7,
44037
44924
  notInAllow: true
44038
44925
  } : {
44039
44926
  category,
@@ -44043,7 +44930,7 @@ function friendlyToolRule({ category, inner }) {
44043
44930
  }
44044
44931
  if (isCatchAllSpecifier(inner)) return {
44045
44932
  category,
44046
- pattern: CATCH_ALL_PATTERN$6,
44933
+ pattern: CATCH_ALL_PATTERN$7,
44047
44934
  notInAllow: false
44048
44935
  };
44049
44936
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -44062,13 +44949,13 @@ function friendlyToolRule({ category, inner }) {
44062
44949
  */
44063
44950
  function parseCommandcodeRule(rule) {
44064
44951
  const { tool, inner } = splitCommandcodeRule(rule);
44065
- if (tool === CATCH_ALL_PATTERN$6) return isCatchAllSpecifier(inner) ? {
44952
+ if (tool === CATCH_ALL_PATTERN$7) return isCatchAllSpecifier(inner) ? {
44066
44953
  category: "*",
44067
- pattern: CATCH_ALL_PATTERN$6,
44954
+ pattern: CATCH_ALL_PATTERN$7,
44068
44955
  notInAllow: true
44069
44956
  } : null;
44070
44957
  const lowered = tool.toLowerCase();
44071
- if (lowered.startsWith(MCP_CANONICAL_PREFIX$5)) return parseMcpCommandcodeRule({
44958
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$6)) return parseMcpCommandcodeRule({
44072
44959
  tool,
44073
44960
  inner
44074
44961
  });
@@ -44086,12 +44973,12 @@ function parseCommandcodeRule(rule) {
44086
44973
  */
44087
44974
  function isFoldedMcpToolNameRule({ rule, category }) {
44088
44975
  const { tool } = splitCommandcodeRule(rule);
44089
- return category.startsWith(MCP_CANONICAL_PREFIX$5) && !tool.startsWith(MCP_CANONICAL_PREFIX$5);
44976
+ return category.startsWith(MCP_CANONICAL_PREFIX$6) && !tool.startsWith(MCP_CANONICAL_PREFIX$6);
44090
44977
  }
44091
44978
  /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
44092
44979
  function scopedMcpRuleTool(rule) {
44093
44980
  const { tool, inner } = splitCommandcodeRule(rule);
44094
- return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$5) ? tool : null;
44981
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$6) ? tool : null;
44095
44982
  }
44096
44983
  /**
44097
44984
  * The string entries of a Command Code permission list. Command Code reads
@@ -44134,7 +45021,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
44134
45021
  }).filter((rule) => {
44135
45022
  const parsed = parseCommandcodeRule(rule);
44136
45023
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
44137
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$6})`);
45024
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$7})`);
44138
45025
  if (key !== "allow" && isStricterAction({
44139
45026
  action: key,
44140
45027
  existing: replacement
@@ -44191,7 +45078,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
44191
45078
  const ranked = /* @__PURE__ */ new Map();
44192
45079
  const managedCategories = /* @__PURE__ */ new Set();
44193
45080
  for (const [category, rules] of Object.entries(permission)) {
44194
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$6);
45081
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$7);
44195
45082
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
44196
45083
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
44197
45084
  for (const [pattern, action] of Object.entries(rules)) {
@@ -44260,7 +45147,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
44260
45147
  function skippedAllowImportMessage({ rule, category }) {
44261
45148
  const { tool, inner } = splitCommandcodeRule(rule);
44262
45149
  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.`;
44263
- 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.`;
45150
+ 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.`;
44264
45151
  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.`;
44265
45152
  }
44266
45153
  /**
@@ -44462,7 +45349,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
44462
45349
  //#endregion
44463
45350
  //#region src/features/permissions/continue-permissions.ts
44464
45351
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
44465
- const CATCH_ALL_PATTERN$5 = "*";
45352
+ const CATCH_ALL_PATTERN$6 = "*";
44466
45353
  /**
44467
45354
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
44468
45355
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -44470,7 +45357,7 @@ const CATCH_ALL_PATTERN$5 = "*";
44470
45357
  * the bare entry instead of being skipped as a pattern-specific rule.
44471
45358
  */
44472
45359
  function normalizeCatchAllPattern$1(pattern) {
44473
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
45360
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$6 : pattern;
44474
45361
  }
44475
45362
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
44476
45363
  bash: "Bash",
@@ -44499,7 +45386,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
44499
45386
  function toContinueToolName(canonical) {
44500
45387
  return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
44501
45388
  }
44502
- function toCanonicalToolName$5(continueName) {
45389
+ function toCanonicalToolName$6(continueName) {
44503
45390
  return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
44504
45391
  }
44505
45392
  /**
@@ -44507,7 +45394,7 @@ function toCanonicalToolName$5(continueName) {
44507
45394
  * `Tool(pattern)`.
44508
45395
  */
44509
45396
  function buildContinuePermissionEntry(toolName, pattern) {
44510
- return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
45397
+ return pattern === CATCH_ALL_PATTERN$6 ? toolName : `${toolName}(${pattern})`;
44511
45398
  }
44512
45399
  /**
44513
45400
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -44521,7 +45408,7 @@ function parseContinuePermissionEntry(entry) {
44521
45408
  if (toolName === "") return;
44522
45409
  return {
44523
45410
  toolName,
44524
- pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
45411
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$6 : pattern
44525
45412
  };
44526
45413
  }
44527
45414
  /**
@@ -44695,7 +45582,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
44695
45582
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
44696
45583
  continue;
44697
45584
  }
44698
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
45585
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$6) {
44699
45586
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
44700
45587
  continue;
44701
45588
  }
@@ -44720,7 +45607,7 @@ function convertContinuePermissionsToRulesync(lists) {
44720
45607
  const parsedEntry = parseContinuePermissionEntry(entry);
44721
45608
  if (parsedEntry === void 0) continue;
44722
45609
  if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
44723
- const category = toCanonicalToolName$5(parsedEntry.toolName);
45610
+ const category = toCanonicalToolName$6(parsedEntry.toolName);
44724
45611
  permission[category] ??= {};
44725
45612
  permission[category][parsedEntry.pattern] ??= action;
44726
45613
  }
@@ -45044,14 +45931,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
45044
45931
  };
45045
45932
  //#endregion
45046
45933
  //#region src/features/permissions/crush-permissions.ts
45047
- const CATCH_ALL_PATTERN$4 = "*";
45934
+ const CATCH_ALL_PATTERN$5 = "*";
45048
45935
  /**
45049
45936
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
45050
45937
  * argument patterns at all, so it is the only pattern a rule can carry and
45051
45938
  * still be written.
45052
45939
  */
45053
45940
  function normalizeCatchAllPattern(pattern) {
45054
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$4 : pattern;
45941
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
45055
45942
  }
45056
45943
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
45057
45944
  bash: "bash",
@@ -45063,20 +45950,20 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
45063
45950
  webfetch: "fetch"
45064
45951
  };
45065
45952
  const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
45066
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
45953
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
45067
45954
  const MCP_CRUSH_PREFIX = "mcp_";
45068
45955
  function toCrushToolName(canonical) {
45069
45956
  if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
45070
- if (canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
45957
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
45071
45958
  return canonical;
45072
45959
  }
45073
- function toCanonicalToolName$4(crushName) {
45960
+ function toCanonicalToolName$5(crushName) {
45074
45961
  return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
45075
45962
  }
45076
45963
  function isMcpToolName(crushName) {
45077
45964
  return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
45078
45965
  }
45079
- function uniq$9(values) {
45966
+ function uniq$10(values) {
45080
45967
  return [...new Set(values)];
45081
45968
  }
45082
45969
  /**
@@ -45182,8 +46069,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
45182
46069
  const managedToolNames = managedCrushToolNames(config);
45183
46070
  const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
45184
46071
  const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
45185
- const allowedList = uniq$9([...preservedAllowed, ...generated.allowed]);
45186
- const disabledList = uniq$9([...preservedDisabled, ...generated.disabled]);
46072
+ const allowedList = uniq$10([...preservedAllowed, ...generated.allowed]);
46073
+ const disabledList = uniq$10([...preservedDisabled, ...generated.disabled]);
45187
46074
  const patch = {};
45188
46075
  if (allowedList.length > 0 || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = { [CRUSH_ALLOWED_TOOLS_KEY]: allowedList.length > 0 ? allowedList : void 0 };
45189
46076
  if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
@@ -45250,7 +46137,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
45250
46137
  for (const [rawPattern, action] of Object.entries(rules)) {
45251
46138
  if (isPrototypePollutionKey(rawPattern)) continue;
45252
46139
  const pattern = normalizeCatchAllPattern(rawPattern);
45253
- if (pattern === CATCH_ALL_PATTERN$4) {
46140
+ if (pattern === CATCH_ALL_PATTERN$5) {
45254
46141
  catchAllAction = action;
45255
46142
  continue;
45256
46143
  }
@@ -45300,8 +46187,8 @@ function convertRulesyncToCrushLists({ config, logger }) {
45300
46187
  else if (catchAllAction === "deny") lists.disabled.push(toolName);
45301
46188
  }
45302
46189
  return {
45303
- allowed: uniq$9(lists.allowed),
45304
- disabled: uniq$9(lists.disabled)
46190
+ allowed: uniq$10(lists.allowed),
46191
+ disabled: uniq$10(lists.disabled)
45305
46192
  };
45306
46193
  }
45307
46194
  /**
@@ -45323,11 +46210,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
45323
46210
  };
45324
46211
  for (const entry of disabled) {
45325
46212
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
45326
- bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] = "deny";
46213
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] = "deny";
45327
46214
  }
45328
46215
  for (const entry of allowed) {
45329
46216
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
45330
- bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] ??= "allow";
46217
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] ??= "allow";
45331
46218
  }
45332
46219
  return { permission };
45333
46220
  }
@@ -45374,13 +46261,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
45374
46261
  WebFetch: "webfetch",
45375
46262
  Mcp: "mcp"
45376
46263
  };
45377
- const MCP_CANONICAL_PREFIX$3 = "mcp__";
46264
+ const MCP_CANONICAL_PREFIX$4 = "mcp__";
45378
46265
  /**
45379
46266
  * Returns true if the canonical category is the per-tool MCP form
45380
46267
  * `mcp__<server>__<tool>`.
45381
46268
  */
45382
46269
  function isMcpScopedCategory(canonical) {
45383
- return canonical.startsWith(MCP_CANONICAL_PREFIX$3) && canonical.length > 5;
46270
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5;
45384
46271
  }
45385
46272
  function toCursorType(canonical) {
45386
46273
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -45414,7 +46301,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
45414
46301
  if (match) {
45415
46302
  const server = match[1] ?? "*";
45416
46303
  const tool = match[2] ?? "*";
45417
- return `${MCP_CANONICAL_PREFIX$3}${server}__${tool}`;
46304
+ return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
45418
46305
  }
45419
46306
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
45420
46307
  }
@@ -45707,7 +46594,7 @@ function convertCursorToRulesyncPermissions(params) {
45707
46594
  const { type, pattern } = parseCursorPermissionEntry(entry);
45708
46595
  const canonical = toCanonicalCategory$1(type, pattern);
45709
46596
  if (!permission[canonical]) permission[canonical] = {};
45710
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$3) ? "*" : pattern;
46597
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$4) ? "*" : pattern;
45711
46598
  permission[canonical][canonicalPattern] = action;
45712
46599
  }
45713
46600
  };
@@ -47226,7 +48113,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
47226
48113
  //#region src/features/permissions/goose-permissions.ts
47227
48114
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
47228
48115
  const GOOSE_USER_KEY = "user";
47229
- const CATCH_ALL_PATTERN$3 = "*";
48116
+ const CATCH_ALL_PATTERN$4 = "*";
47230
48117
  const RULESYNC_TO_GOOSE_TOOL_NAME = {
47231
48118
  bash: "developer__shell",
47232
48119
  edit: "developer__text_editor",
@@ -47388,7 +48275,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
47388
48275
  for (const [category, rules] of orderedEntries) {
47389
48276
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
47390
48277
  for (const [pattern, action] of Object.entries(rules)) {
47391
- if (pattern !== CATCH_ALL_PATTERN$3) {
48278
+ if (pattern !== CATCH_ALL_PATTERN$4) {
47392
48279
  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.`);
47393
48280
  continue;
47394
48281
  }
@@ -47417,7 +48304,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
47417
48304
  for (const toolName of toolNames) {
47418
48305
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
47419
48306
  permission[category] ??= {};
47420
- permission[category][CATCH_ALL_PATTERN$3] = action;
48307
+ permission[category][CATCH_ALL_PATTERN$4] = action;
47421
48308
  }
47422
48309
  }
47423
48310
  return { permission };
@@ -47428,8 +48315,8 @@ const GROKCLI_UI_KEY = "ui";
47428
48315
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
47429
48316
  const GROKCLI_PERMISSION_KEY = "permission";
47430
48317
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
47431
- const CATCH_ALL_PATTERN$2 = "*";
47432
- const MCP_CANONICAL_PREFIX$2 = "mcp__";
48318
+ const CATCH_ALL_PATTERN$3 = "*";
48319
+ const MCP_CANONICAL_PREFIX$3 = "mcp__";
47433
48320
  const CATEGORY_TO_GROK_TOOL = {
47434
48321
  bash: "Bash",
47435
48322
  read: "Read",
@@ -47470,14 +48357,14 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
47470
48357
  * concrete pattern emits `Tool(pattern)`.
47471
48358
  */
47472
48359
  function buildGrokEntry(category, pattern) {
47473
- if (category.startsWith(MCP_CANONICAL_PREFIX$2)) {
48360
+ if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
47474
48361
  const remainder = category.slice(5);
47475
48362
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
47476
48363
  }
47477
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
48364
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
47478
48365
  const tool = CATEGORY_TO_GROK_TOOL[category];
47479
48366
  if (tool === void 0) return null;
47480
- return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
48367
+ return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? tool : `${tool}(${pattern})`;
47481
48368
  }
47482
48369
  /**
47483
48370
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -47497,17 +48384,17 @@ function parseGrokEntry(entry) {
47497
48384
  inner = trimmed.slice(parenIndex + 1, -1).trim();
47498
48385
  }
47499
48386
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
47500
- category: `${MCP_CANONICAL_PREFIX$2}${inner}`,
47501
- pattern: CATCH_ALL_PATTERN$2
48387
+ category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
48388
+ pattern: CATCH_ALL_PATTERN$3
47502
48389
  } : {
47503
48390
  category: "mcp",
47504
- pattern: CATCH_ALL_PATTERN$2
48391
+ pattern: CATCH_ALL_PATTERN$3
47505
48392
  };
47506
48393
  const category = GROK_TOOL_TO_CATEGORY[tool];
47507
48394
  if (category === void 0) return null;
47508
48395
  return {
47509
48396
  category,
47510
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
48397
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$3
47511
48398
  };
47512
48399
  }
47513
48400
  /**
@@ -47528,15 +48415,15 @@ function parseGrokRule(rule) {
47528
48415
  if (typeof tool !== "string") return null;
47529
48416
  if (pattern !== void 0 && typeof pattern !== "string") return null;
47530
48417
  const trimmedPattern = pattern?.trim() ?? "";
47531
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
48418
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$3;
47532
48419
  const lowerTool = tool.trim().toLowerCase();
47533
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
48420
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$3 ? {
47534
48421
  category: "mcp",
47535
- pattern: CATCH_ALL_PATTERN$2,
48422
+ pattern: CATCH_ALL_PATTERN$3,
47536
48423
  action
47537
48424
  } : {
47538
- category: `${MCP_CANONICAL_PREFIX$2}${resolvedPattern}`,
47539
- pattern: CATCH_ALL_PATTERN$2,
48425
+ category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
48426
+ pattern: CATCH_ALL_PATTERN$3,
47540
48427
  action
47541
48428
  };
47542
48429
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -47684,7 +48571,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
47684
48571
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
47685
48572
  }
47686
48573
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
47687
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
48574
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$3]: legacyModeAction(parsed) } } };
47688
48575
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
47689
48576
  }
47690
48577
  validate() {
@@ -49564,6 +50451,567 @@ var PiPermissions = class PiPermissions extends ToolPermissions {
49564
50451
  }
49565
50452
  };
49566
50453
  //#endregion
50454
+ //#region src/features/permissions/pool-permissions.ts
50455
+ const POOL_ALLOW_KEY = "allow";
50456
+ const POOL_DENY_KEY = "deny";
50457
+ const POOL_DISABLED_KEY = "disabled";
50458
+ const POOL_PATH_KEY = "path";
50459
+ const POOL_WRITE_KEY = "write";
50460
+ const CATCH_ALL_PATTERN$2 = "*";
50461
+ const PATHS_CATCH_ALL_PATTERN = "**";
50462
+ const CANONICAL_TO_POOL_TOOL_NAMES = {
50463
+ bash: "shell",
50464
+ webfetch: "web_fetch",
50465
+ websearch: "web_search"
50466
+ };
50467
+ const POOL_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_TOOL_NAMES).map(([k, v]) => [v, k]));
50468
+ const READ_CATEGORY = "read";
50469
+ const WRITE_CATEGORIES = ["edit", "write"];
50470
+ const FILE_CATEGORIES = [READ_CATEGORY, ...WRITE_CATEGORIES];
50471
+ const MCP_CANONICAL_PREFIX$2 = "mcp__";
50472
+ /**
50473
+ * Single spelling of the settings.yaml codec/policy, matching the
50474
+ * `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
50475
+ * unparseable root rather than replacing the user's primary Pool settings with
50476
+ * generated output.
50477
+ */
50478
+ function parsePoolSettings$1(fileContent, filePath) {
50479
+ return parseSharedConfig({
50480
+ format: "yaml",
50481
+ fileContent,
50482
+ filePath,
50483
+ invalidRootPolicy: "error"
50484
+ });
50485
+ }
50486
+ function toPoolToolName(canonical) {
50487
+ return lookupOwn({
50488
+ record: CANONICAL_TO_POOL_TOOL_NAMES,
50489
+ key: canonical
50490
+ }) ?? canonical;
50491
+ }
50492
+ function toCanonicalToolName$4(poolName) {
50493
+ return lookupOwn({
50494
+ record: POOL_TO_CANONICAL_TOOL_NAMES,
50495
+ key: poolName
50496
+ }) ?? poolName;
50497
+ }
50498
+ function isMcpCategory(category) {
50499
+ return category.startsWith(MCP_CANONICAL_PREFIX$2) && category.length > 5;
50500
+ }
50501
+ function isFileCategory(category) {
50502
+ return FILE_CATEGORIES.includes(category);
50503
+ }
50504
+ /**
50505
+ * Pool's tool rules know one wildcard, `*`, which matches anything including
50506
+ * `/` — the same set a canonical `*` names — so a `**` run is collapsed to
50507
+ * keep the file in Pool's own spelling. `?`, `[...]` and `{a,b}` are literal
50508
+ * characters to Pool, so a pattern that relies on them cannot be written.
50509
+ */
50510
+ function toPoolToolPattern(pattern) {
50511
+ if (/[?[{]/.test(pattern)) return void 0;
50512
+ return pattern.replaceAll(/\*{2,}/g, "*");
50513
+ }
50514
+ /**
50515
+ * Pool's path globs know `*` and `**`, so a bare `*` run is written as `**`
50516
+ * to keep it matching anything. `?`, `[...]` and `{a,b}` are literal
50517
+ * characters to Pool here as well, so a pattern that relies on them cannot be
50518
+ * written.
50519
+ */
50520
+ function toPoolPathPattern(pattern) {
50521
+ if (/[?[{]/.test(pattern)) return void 0;
50522
+ return /^\*+$/.test(pattern) ? PATHS_CATCH_ALL_PATTERN : pattern;
50523
+ }
50524
+ function isHomeOrAbsolutePath(pattern) {
50525
+ return (0, node_path.isAbsolute)(pattern) || pattern === "~" || pattern.startsWith("~/");
50526
+ }
50527
+ function rulesOf(permission, category) {
50528
+ return lookupOwn({
50529
+ record: permission,
50530
+ key: category
50531
+ }) ?? {};
50532
+ }
50533
+ function patternsWithAction(rules, actions) {
50534
+ return Object.entries(rules).filter(([pattern, action]) => !isPrototypePollutionKey(pattern) && actions.includes(action)).map(([pattern]) => pattern);
50535
+ }
50536
+ function asRestrictions(patterns) {
50537
+ return (0, es_toolkit.uniq)(patterns).map((pattern) => ({
50538
+ pattern,
50539
+ fromAllToolsCategory: false
50540
+ }));
50541
+ }
50542
+ function listPatterns(patterns) {
50543
+ return patterns.map((pattern) => quoteValueForWarning(pattern)).join(", ");
50544
+ }
50545
+ /**
50546
+ * Permissions adapter for Pool (Poolside's coding agent CLI).
50547
+ *
50548
+ * Pool keeps its permission rules in the same YAML settings file as its MCP
50549
+ * servers — `.poolside/settings.yaml` (project) / `~/.config/poolside/settings.yaml`
50550
+ * (global) — under two top-level keys:
50551
+ * - `tools.<name>`: `allow` and `deny` pattern lists matched against the
50552
+ * tool's argument (the command for `shell`, the URL for `web_fetch`, ...),
50553
+ * plus `disabled: true` to remove the tool. A denied pattern wins over an
50554
+ * allowed one; a call that matches neither prompts. Only `*` is a wildcard.
50555
+ * - `paths`: `allow` entries (`{ path }`, read-only unless `write: true`)
50556
+ * and `deny` entries (`{ path }`) with `*`/`**` globs; deny wins.
50557
+ *
50558
+ * Mapping (rulesync canonical -> Pool):
50559
+ * - `bash` -> `tools.shell`, `webfetch` -> `tools.web_fetch`,
50560
+ * `websearch` -> `tools.web_search`; any other non-file category passes
50561
+ * through as `tools.<category>`. `allow` -> `allow`, `deny` -> `deny`,
50562
+ * `ask` -> nothing (prompting is Pool's default) — but an `allow` that
50563
+ * overlaps an `ask` of any category landing on the same Pool tool, or a
50564
+ * deny Pool cannot spell, is withheld, since an allowed pattern would
50565
+ * silence the prompt the config asks for.
50566
+ * - `read` / `edit` / `write` -> `paths`: a `read` allow is a read-only
50567
+ * entry, an `edit` or `write` allow adds `write: true`, a `read` deny is a
50568
+ * deny entry. Pool cannot deny writes without denying reads, so an
50569
+ * `edit`/`write` deny that is not also a `read` deny only withholds the
50570
+ * write flag of the entries it overlaps (Pool prompts for those writes).
50571
+ * - `mcp__<server>__<tool>` is skipped: Pool approves MCP tools per server
50572
+ * under `mcp_servers.<server>.allow`, authored as `poolAllow` in
50573
+ * `.rulesync/mcp.json`. The all-tools `*` category is mirrored onto
50574
+ * `bash` by `honorAllToolsOnBash` and otherwise skipped.
50575
+ *
50576
+ * Project settings take project-relative paths and the user settings absolute
50577
+ * or `~` paths, so a path of the other kind is skipped with a warning at each
50578
+ * scope. The managed tools' `allow`/`deny` lists and, whenever the canonical
50579
+ * config carries a file category, the `paths` lists are rebuilt; every other
50580
+ * key (`disabled`, unmanaged tools, `mcp_servers`, `pool`, `sandbox`, ...) is
50581
+ * kept, and the file is never deleted.
50582
+ *
50583
+ * @see https://docs.poolside.ai/settings-file-reference
50584
+ */
50585
+ var PoolPermissions = class PoolPermissions extends ToolPermissions {
50586
+ settings;
50587
+ constructor(params) {
50588
+ super({
50589
+ ...params,
50590
+ fileContent: params.fileContent ?? ""
50591
+ });
50592
+ this.settings = parsePoolSettings$1(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
50593
+ }
50594
+ getSettings() {
50595
+ return this.settings;
50596
+ }
50597
+ isDeletable() {
50598
+ return false;
50599
+ }
50600
+ /**
50601
+ * settings.yaml is Pool's file: rulesync merges into it when it exists but
50602
+ * does not create one that holds nothing of its own — an absent file and an
50603
+ * absent rule both mean "prompt".
50604
+ */
50605
+ shouldSkipCreationWhenPayloadEmpty() {
50606
+ return true;
50607
+ }
50608
+ static getSettablePaths({ global = false } = {}) {
50609
+ return {
50610
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
50611
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
50612
+ };
50613
+ }
50614
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
50615
+ const paths = this.getSettablePaths({ global });
50616
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
50617
+ return new PoolPermissions({
50618
+ outputRoot,
50619
+ relativeDirPath: paths.relativeDirPath,
50620
+ relativeFilePath: paths.relativeFilePath,
50621
+ fileContent,
50622
+ validate,
50623
+ global
50624
+ });
50625
+ }
50626
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
50627
+ const paths = this.getSettablePaths({ global });
50628
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
50629
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
50630
+ const existing = parsePoolSettings$1(existingContent, filePath);
50631
+ const patch = buildPoolPermissionsPatch({
50632
+ config: rulesyncPermissions.getJson(),
50633
+ existing,
50634
+ global,
50635
+ logger
50636
+ });
50637
+ return new PoolPermissions({
50638
+ outputRoot,
50639
+ relativeDirPath: paths.relativeDirPath,
50640
+ relativeFilePath: paths.relativeFilePath,
50641
+ fileContent: applySharedConfigPatch({
50642
+ fileKey: sharedConfigFileKey(paths),
50643
+ feature: "permissions",
50644
+ existingContent,
50645
+ patch,
50646
+ filePath,
50647
+ logger
50648
+ }),
50649
+ validate: true,
50650
+ global
50651
+ });
50652
+ }
50653
+ toRulesyncPermissions() {
50654
+ const rulesyncConfig = convertPoolToRulesync({
50655
+ tools: isRecord$1(this.settings["tools"]) ? this.settings[POOL_TOOLS_KEY] : {},
50656
+ paths: isRecord$1(this.settings["paths"]) ? this.settings[POOL_PATHS_KEY] : {}
50657
+ });
50658
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
50659
+ }
50660
+ validate() {
50661
+ return {
50662
+ success: true,
50663
+ error: null
50664
+ };
50665
+ }
50666
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50667
+ return new PoolPermissions({
50668
+ outputRoot,
50669
+ relativeDirPath,
50670
+ relativeFilePath,
50671
+ fileContent: "",
50672
+ validate: false,
50673
+ global
50674
+ });
50675
+ }
50676
+ };
50677
+ /**
50678
+ * The deep-merge patch that rewrites the managed `tools.<name>` lists and,
50679
+ * when the canonical config carries a file category, the `paths` lists. An
50680
+ * empty list retracts its key (`undefined` deletes under deep-merge), and a
50681
+ * block is only touched when there is something to write into it or it
50682
+ * already exists, so a config without rules does not grow an empty
50683
+ * `tools: {}` / `paths: {}`.
50684
+ */
50685
+ function buildPoolPermissionsPatch({ config, existing, global, logger }) {
50686
+ const permission = honorAllToolsOnBash(config.permission);
50687
+ const existingTools = isRecord$1(existing["tools"]) ? existing[POOL_TOOLS_KEY] : void 0;
50688
+ const existingPaths = isRecord$1(existing["paths"]) ? existing[POOL_PATHS_KEY] : void 0;
50689
+ const patch = {};
50690
+ const toolsPatch = {};
50691
+ for (const [toolName, lists] of convertRulesyncToPoolTools({
50692
+ permission,
50693
+ logger
50694
+ })) {
50695
+ const existingTool = existingTools === void 0 ? void 0 : lookupOwn({
50696
+ record: existingTools,
50697
+ key: toolName
50698
+ });
50699
+ if (lists.allow.length === 0 && lists.deny.length === 0 && existingTool === void 0) continue;
50700
+ toolsPatch[toolName] = {
50701
+ [POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
50702
+ [POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
50703
+ };
50704
+ }
50705
+ if (Object.keys(toolsPatch).length > 0) patch[POOL_TOOLS_KEY] = toolsPatch;
50706
+ if (FILE_CATEGORIES.some((category) => lookupOwn({
50707
+ record: permission,
50708
+ key: category
50709
+ }))) {
50710
+ const lists = convertRulesyncToPoolPaths({
50711
+ permission,
50712
+ global,
50713
+ logger
50714
+ });
50715
+ if (lists.allow.length > 0 || lists.deny.length > 0 || existingPaths !== void 0) patch[POOL_PATHS_KEY] = {
50716
+ [POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
50717
+ [POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
50718
+ };
50719
+ }
50720
+ return patch;
50721
+ }
50722
+ /**
50723
+ * Convert one non-file category into a Pool tool's `allow`/`deny` lists. A
50724
+ * deny Pool can spell needs no withholding (Pool lets it win over an allow),
50725
+ * but an `ask` has no list of its own — a call Pool does not match prompts —
50726
+ * so an allow that overlaps one is withheld and reported, and the tool keeps
50727
+ * prompting. A deny Pool cannot spell (its pattern uses a glob form other
50728
+ * than `*`) is skipped, and then withholds the allows it overlaps the same
50729
+ * way: written, such an allow would auto-approve the very command the deny
50730
+ * was meant to stop. `restrictions` carries the ask patterns and the skipped
50731
+ * deny patterns of every category that lands on the same Pool tool, because
50732
+ * Pool matches the joined lists as one.
50733
+ */
50734
+ function convertToolCategory({ category, toolName, rules, restrictions, logger }) {
50735
+ const lists = {
50736
+ allow: [],
50737
+ deny: []
50738
+ };
50739
+ const shadowingRestrictions = createShadowingRestrictionsTest(asRestrictions(restrictions));
50740
+ for (const [rawPattern, action] of Object.entries(rules)) {
50741
+ if (isPrototypePollutionKey(rawPattern) || action === "ask") continue;
50742
+ const pattern = toPoolToolPattern(rawPattern);
50743
+ if (pattern === void 0) {
50744
+ logger?.warn(`Pool tool rules treat only "*" as a wildcard, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
50745
+ continue;
50746
+ }
50747
+ if (action === "deny") {
50748
+ lists.deny.push(pattern);
50749
+ continue;
50750
+ }
50751
+ const shadowing = shadowingRestrictions(rawPattern);
50752
+ if (shadowing.length > 0) {
50753
+ 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}".`);
50754
+ continue;
50755
+ }
50756
+ lists.allow.push(pattern);
50757
+ }
50758
+ return {
50759
+ allow: (0, es_toolkit.uniq)(lists.allow),
50760
+ deny: (0, es_toolkit.uniq)(lists.deny)
50761
+ };
50762
+ }
50763
+ function unrepresentableDenies(rules) {
50764
+ return patternsWithAction(rules, ["deny"]).filter((pattern) => toPoolToolPattern(pattern) === void 0);
50765
+ }
50766
+ /**
50767
+ * Group the non-file categories by the Pool tool they land on, reporting the
50768
+ * ones Pool has no place for. Two categories can resolve to the same tool
50769
+ * (`bash` and a pass-through `shell`).
50770
+ */
50771
+ function groupCategoriesByPoolTool({ permission, logger }) {
50772
+ const byTool = /* @__PURE__ */ new Map();
50773
+ for (const [category, rules] of Object.entries(permission)) {
50774
+ if (isPrototypePollutionKey(category) || isFileCategory(category)) continue;
50775
+ if (category === "*") {
50776
+ for (const [rawPattern, action] of Object.entries(rules)) {
50777
+ if (isPrototypePollutionKey(rawPattern)) continue;
50778
+ logger?.warn(`Pool has no rule that spans every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
50779
+ }
50780
+ continue;
50781
+ }
50782
+ if (isMcpCategory(category)) {
50783
+ 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.`);
50784
+ continue;
50785
+ }
50786
+ const toolName = toPoolToolName(category);
50787
+ byTool.set(toolName, [...byTool.get(toolName) ?? [], [category, rules]]);
50788
+ }
50789
+ return byTool;
50790
+ }
50791
+ /**
50792
+ * Convert the non-file categories into Pool tool lists, keyed by Pool tool
50793
+ * name. The categories that land on the same tool are converted against the
50794
+ * union of their `ask` patterns and their lists are joined, which is safe
50795
+ * because a deny wins over an allow in Pool whichever category wrote it.
50796
+ */
50797
+ function convertRulesyncToPoolTools({ permission, logger }) {
50798
+ const byTool = /* @__PURE__ */ new Map();
50799
+ for (const [toolName, categories] of groupCategoriesByPoolTool({
50800
+ permission,
50801
+ logger
50802
+ })) {
50803
+ const restrictions = categories.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...unrepresentableDenies(rules)]);
50804
+ const lists = {
50805
+ allow: [],
50806
+ deny: []
50807
+ };
50808
+ for (const [category, rules] of categories) {
50809
+ const converted = convertToolCategory({
50810
+ category,
50811
+ toolName,
50812
+ rules,
50813
+ restrictions,
50814
+ logger
50815
+ });
50816
+ lists.allow.push(...converted.allow);
50817
+ lists.deny.push(...converted.deny);
50818
+ }
50819
+ byTool.set(toolName, {
50820
+ allow: (0, es_toolkit.uniq)(lists.allow),
50821
+ deny: (0, es_toolkit.uniq)(lists.deny)
50822
+ });
50823
+ }
50824
+ return byTool;
50825
+ }
50826
+ /**
50827
+ * Convert the file categories into Pool's `paths` lists.
50828
+ *
50829
+ * An allow entry grants reading, and `write: true` grants writing on top, so
50830
+ * a `read` allow is withheld by an overlapping `read` ask and a write allow by
50831
+ * an overlapping `edit`/`write` ask — and by a `read` ask, since the write
50832
+ * entry would read too. A `read` deny lands in `paths.deny`, which Pool lets
50833
+ * win over any allow, and covers an `edit`/`write` deny of the same pattern,
50834
+ * so neither withholds anything; an `edit`/`write` deny without a `read` deny
50835
+ * cannot be written (Pool's deny blocks reads as well), so it is reported and
50836
+ * only withholds the write flags it overlaps. A deny Pool cannot spell is
50837
+ * skipped the same way and withholds the allows it overlaps, so it is honored
50838
+ * by prompting rather than dropped.
50839
+ */
50840
+ function convertRulesyncToPoolPaths({ permission, global, logger }) {
50841
+ const readRules = rulesOf(permission, READ_CATEGORY);
50842
+ const writeRulesByCategory = WRITE_CATEGORIES.map((category) => [category, rulesOf(permission, category)]);
50843
+ const readAsks = patternsWithAction(readRules, ["ask"]);
50844
+ const fitsScope = ({ category, action, pattern }) => {
50845
+ if (isHomeOrAbsolutePath(pattern) === global) return true;
50846
+ 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 ${(0, node_path.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 ~/${(0, node_path.join)(POOL_GLOBAL_DIR, POOL_SETTINGS_FILE_NAME)} (via --global) instead.`);
50847
+ return false;
50848
+ };
50849
+ const deny = [];
50850
+ const readRestrictions = [...readAsks];
50851
+ for (const pattern of patternsWithAction(readRules, ["deny"])) {
50852
+ const path = toPoolPathPattern(pattern);
50853
+ if (path === void 0) {
50854
+ warnSkippedPathPattern({
50855
+ category: READ_CATEGORY,
50856
+ action: "deny",
50857
+ pattern,
50858
+ logger
50859
+ });
50860
+ readRestrictions.push(pattern);
50861
+ continue;
50862
+ }
50863
+ if (!fitsScope({
50864
+ category: READ_CATEGORY,
50865
+ action: "deny",
50866
+ pattern
50867
+ })) continue;
50868
+ if (!deny.some((entry) => entry[POOL_PATH_KEY] === path)) deny.push({ [POOL_PATH_KEY]: path });
50869
+ }
50870
+ const isDeniedForRead = (pattern) => {
50871
+ const path = toPoolPathPattern(pattern);
50872
+ return path !== void 0 && deny.some((entry) => entry[POOL_PATH_KEY] === path);
50873
+ };
50874
+ const writeRestrictions = writeRulesByCategory.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...patternsWithAction(rules, ["deny"]).filter((pattern) => !isDeniedForRead(pattern))]);
50875
+ reportUnwrittenWriteDenies({
50876
+ writeRulesByCategory,
50877
+ isDeniedForRead,
50878
+ logger
50879
+ });
50880
+ const shadowingReadRestrictions = createShadowingRestrictionsTest(asRestrictions(readRestrictions));
50881
+ const shadowingWriteRestrictions = createShadowingRestrictionsTest(asRestrictions([...readRestrictions, ...writeRestrictions]));
50882
+ const allowByPath = /* @__PURE__ */ new Map();
50883
+ for (const pattern of patternsWithAction(readRules, ["allow"])) {
50884
+ if (!fitsScope({
50885
+ category: READ_CATEGORY,
50886
+ action: "allow",
50887
+ pattern
50888
+ })) continue;
50889
+ const path = toPoolPathPattern(pattern);
50890
+ if (path === void 0) {
50891
+ warnSkippedPathPattern({
50892
+ category: READ_CATEGORY,
50893
+ action: "allow",
50894
+ pattern,
50895
+ logger
50896
+ });
50897
+ continue;
50898
+ }
50899
+ const shadowing = shadowingReadRestrictions(pattern);
50900
+ if (shadowing.length > 0) {
50901
+ 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)}.`);
50902
+ continue;
50903
+ }
50904
+ allowByPath.set(path, { [POOL_PATH_KEY]: path });
50905
+ }
50906
+ for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["allow"])) {
50907
+ if (!fitsScope({
50908
+ category,
50909
+ action: "allow",
50910
+ pattern
50911
+ })) continue;
50912
+ const path = toPoolPathPattern(pattern);
50913
+ if (path === void 0) {
50914
+ warnSkippedPathPattern({
50915
+ category,
50916
+ action: "allow",
50917
+ pattern,
50918
+ logger
50919
+ });
50920
+ continue;
50921
+ }
50922
+ const shadowing = shadowingWriteRestrictions(pattern);
50923
+ if (shadowing.length > 0) {
50924
+ 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.`);
50925
+ continue;
50926
+ }
50927
+ allowByPath.set(path, {
50928
+ [POOL_PATH_KEY]: path,
50929
+ [POOL_WRITE_KEY]: true
50930
+ });
50931
+ }
50932
+ return {
50933
+ allow: [...allowByPath.values()],
50934
+ deny
50935
+ };
50936
+ }
50937
+ function warnSkippedPathPattern({ category, action, pattern, logger }) {
50938
+ logger?.warn(`Pool path rules treat only "*" and "**" as wildcards, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
50939
+ }
50940
+ function reportUnwrittenWriteDenies({ writeRulesByCategory, isDeniedForRead, logger }) {
50941
+ for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["deny"])) {
50942
+ if (isDeniedForRead(pattern)) continue;
50943
+ if (toPoolPathPattern(pattern) === void 0) {
50944
+ warnSkippedPathPattern({
50945
+ category,
50946
+ action: "deny",
50947
+ pattern,
50948
+ logger
50949
+ });
50950
+ continue;
50951
+ }
50952
+ 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.`);
50953
+ }
50954
+ }
50955
+ function isUsablePattern(pattern) {
50956
+ return typeof pattern === "string" && pattern !== "" && !isPrototypePollutionKey(pattern);
50957
+ }
50958
+ function pathOfEntry(entry) {
50959
+ return isRecord$1(entry) && isUsablePattern(entry[POOL_PATH_KEY]) ? entry[POOL_PATH_KEY] : void 0;
50960
+ }
50961
+ /**
50962
+ * Convert one `tools.<name>` block into its canonical bucket. A disabled tool
50963
+ * never runs, so it reads as a catch-all deny and its own lists are ignored;
50964
+ * a denied pattern wins over an allowed one, as it does in Pool.
50965
+ */
50966
+ function importPoolTool({ toolConfig, bucket }) {
50967
+ if (toolConfig[POOL_DISABLED_KEY] === true) {
50968
+ bucket[CATCH_ALL_PATTERN$2] = "deny";
50969
+ return;
50970
+ }
50971
+ const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
50972
+ const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
50973
+ for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
50974
+ for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
50975
+ }
50976
+ /**
50977
+ * Convert Pool's `tools` and `paths` blocks back into a rulesync config.
50978
+ * `mcp_servers` approvals are the mcp feature's (`poolAllow`) and are not read.
50979
+ */
50980
+ function convertPoolToRulesync({ tools, paths }) {
50981
+ const permission = {};
50982
+ const bucketFor = (category) => {
50983
+ const own = lookupOwn({
50984
+ record: permission,
50985
+ key: category
50986
+ });
50987
+ if (own !== void 0) return own;
50988
+ const created = {};
50989
+ permission[category] = created;
50990
+ return created;
50991
+ };
50992
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
50993
+ if (isPrototypePollutionKey(toolName) || !isRecord$1(toolConfig)) continue;
50994
+ importPoolTool({
50995
+ toolConfig,
50996
+ bucket: bucketFor(toCanonicalToolName$4(toolName))
50997
+ });
50998
+ }
50999
+ const denyEntries = Array.isArray(paths[POOL_DENY_KEY]) ? paths[POOL_DENY_KEY] : [];
51000
+ const allowEntries = Array.isArray(paths[POOL_ALLOW_KEY]) ? paths[POOL_ALLOW_KEY] : [];
51001
+ for (const entry of denyEntries) {
51002
+ const path = pathOfEntry(entry);
51003
+ if (path === void 0) continue;
51004
+ for (const category of FILE_CATEGORIES) bucketFor(category)[path] = "deny";
51005
+ }
51006
+ for (const entry of allowEntries) {
51007
+ const path = pathOfEntry(entry);
51008
+ if (path === void 0) continue;
51009
+ bucketFor(READ_CATEGORY)[path] ??= "allow";
51010
+ if (isRecord$1(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
51011
+ }
51012
+ return { permission };
51013
+ }
51014
+ //#endregion
49567
51015
  //#region src/features/permissions/qwencode-permissions.ts
49568
51016
  /**
49569
51017
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -55666,6 +57114,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
55666
57114
  supportsImport: true
55667
57115
  }
55668
57116
  }],
57117
+ ["pool", {
57118
+ class: PoolPermissions,
57119
+ meta: {
57120
+ supportsProject: true,
57121
+ supportsGlobal: true,
57122
+ supportsImport: true
57123
+ }
57124
+ }],
55669
57125
  ["qwencode", {
55670
57126
  class: QwencodePermissions,
55671
57127
  meta: {
@@ -56329,8 +57785,19 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
56329
57785
  const changedPaths = [];
56330
57786
  for (const aiDir of aiDirs) {
56331
57787
  const dirPath = aiDir.getDirPath();
56332
- let dirHasChanges = false;
56333
57788
  const mainFile = aiDir.getMainFile();
57789
+ const otherFiles = aiDir.getOtherFiles();
57790
+ const writtenPaths = [
57791
+ dirPath,
57792
+ ...mainFile ? [(0, node_path.join)(dirPath, mainFile.name)] : [],
57793
+ ...otherFiles.map((file) => (0, node_path.join)(dirPath, file.relativeFilePathToDirPath))
57794
+ ];
57795
+ if (await refusesAnyWriteOutsideRoot({
57796
+ logger: this.logger,
57797
+ rootPath: aiDir.getOutputRoot(),
57798
+ targetPaths: writtenPaths
57799
+ })) continue;
57800
+ let dirHasChanges = false;
56334
57801
  let mainFileContent;
56335
57802
  if (mainFile) {
56336
57803
  const mainFilePath = (0, node_path.join)(dirPath, mainFile.name);
@@ -56342,7 +57809,6 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
56342
57809
  existing: existingContent
56343
57810
  })) dirHasChanges = true;
56344
57811
  }
56345
- const otherFiles = aiDir.getOtherFiles();
56346
57812
  for (const file of otherFiles) {
56347
57813
  if (dirHasChanges) break;
56348
57814
  const filePath = (0, node_path.join)(dirPath, file.relativeFilePathToDirPath);
@@ -56787,7 +58253,7 @@ const AmpSkillFrontmatterSchema = zod_mini.z.looseObject({
56787
58253
  * Amp natively reads Agent Skills following the open standard: a directory with a
56788
58254
  * `SKILL.md` carrying `name`/`description` frontmatter, scoped to the project
56789
58255
  * (`.agents/skills/`) and user-wide (`~/.config/agents/skills/`).
56790
- * @see https://ampcode.com/manual
58256
+ * @see https://ampcode.com/docs/customize/skills
56791
58257
  */
56792
58258
  var AmpSkill = class AmpSkill extends ToolSkill {
56793
58259
  constructor({ outputRoot = process.cwd(), relativeDirPath = AMP_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -61756,37 +63222,6 @@ var PiSkill = class PiSkill extends ToolSkill {
61756
63222
  }
61757
63223
  };
61758
63224
  //#endregion
61759
- //#region src/constants/pool-paths.ts
61760
- /**
61761
- * Pool (Poolside's coding agent CLI) configuration-layout conventions.
61762
- *
61763
- * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
61764
- * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
61765
- * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
61766
- * path), the project-root `AGENTS.md`, and nested per-directory
61767
- * `AGENTS.md` files from the repository root down through the working
61768
- * directory, deeper files taking precedence. It skips ignored directories
61769
- * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
61770
- *
61771
- * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
61772
- * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
61773
- * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
61774
- * the skill directories of other Agent Skills tools; rulesync writes only the
61775
- * Pool-specific roots and leaves the shared ones to their own targets.
61776
- *
61777
- * @see https://docs.poolside.ai/agent-instructions
61778
- * @see https://docs.poolside.ai/skills
61779
- * @see https://github.com/poolsideai/pool
61780
- */
61781
- /** Project-scoped `.poolside/` directory at the project root. */
61782
- const POOL_DIR = ".poolside";
61783
- /** Global config directory for Pool, relative to the home directory. */
61784
- const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
61785
- /** Project skills root, relative to the project root. */
61786
- const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
61787
- /** Global skills root, relative to the home directory. */
61788
- const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
61789
- //#endregion
61790
63225
  //#region src/features/skills/pool-skill.ts
61791
63226
  const PoolSkillFrontmatterSchema = zod_mini.z.looseObject({
61792
63227
  name: zod_mini.z.string(),
@@ -68492,6 +69927,325 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
68492
69927
  }
68493
69928
  };
68494
69929
  //#endregion
69930
+ //#region src/features/subagents/pool-subagent.ts
69931
+ /** How Pool runs an agent; a custom agent defaults to `in_process`. */
69932
+ const POOL_AGENT_TYPES = [
69933
+ "in_process",
69934
+ "command",
69935
+ "agent_server"
69936
+ ];
69937
+ const DEFAULT_POOL_AGENT_TYPE = "in_process";
69938
+ /**
69939
+ * One entry of `subagents.agents.<name>` in Pool's settings file. `type` is
69940
+ * required by Pool and `description` by every enabled custom agent; the
69941
+ * remaining keys depend on the type (`command`/`args`/`env` for `command`,
69942
+ * `agent_server`/`session_config_options` for `agent_server`). Loose so a key
69943
+ * Pool adds later survives the round-trip.
69944
+ * @see https://docs.poolside.ai/subagents
69945
+ * @see https://docs.poolside.ai/settings-file-reference
69946
+ */
69947
+ const PoolAgentSchema = zod_mini.z.looseObject({
69948
+ type: zod_mini.z.optional(zod_mini.z.enum(POOL_AGENT_TYPES)),
69949
+ description: zod_mini.z.optional(zod_mini.z.string()),
69950
+ instructions: zod_mini.z.optional(zod_mini.z.string()),
69951
+ command: zod_mini.z.optional(zod_mini.z.string()),
69952
+ args: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
69953
+ env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
69954
+ agent_server: zod_mini.z.optional(zod_mini.z.string()),
69955
+ session_config_options: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.unknown())),
69956
+ inherit_agent_config: zod_mini.z.optional(zod_mini.z.boolean()),
69957
+ disabled: zod_mini.z.optional(zod_mini.z.boolean())
69958
+ });
69959
+ /**
69960
+ * Single spelling of the settings.yaml codec/policy, matching PoolMcp: fail
69961
+ * closed on an unparseable root rather than replacing the user's primary Pool
69962
+ * settings with generated output.
69963
+ */
69964
+ function parsePoolSettings(fileContent, filePath) {
69965
+ return parseSharedConfig({
69966
+ format: "yaml",
69967
+ fileContent,
69968
+ filePath,
69969
+ invalidRootPolicy: "error"
69970
+ });
69971
+ }
69972
+ /**
69973
+ * The file stem an imported agent lands under in `.rulesync/subagents/`. Pool
69974
+ * keys agents by an arbitrary YAML mapping key, so the name is narrowed to a
69975
+ * safe file name before it becomes a path (defense in depth on top of the
69976
+ * central path-traversal guard); the agent keeps its real name in `name`.
69977
+ */
69978
+ function sanitizePoolAgentFileStem(raw) {
69979
+ const stem = raw.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
69980
+ return stem.length > 0 ? stem : "agent";
69981
+ }
69982
+ /**
69983
+ * Read `subagents.agents` out of a parsed settings document. The reserved
69984
+ * `general` agent is Pool's built-in and stays the user's to configure, so it
69985
+ * is never imported; an entry that is not a mapping is skipped.
69986
+ */
69987
+ function agentsOfSettings({ settings, filePath, logger }) {
69988
+ const block = settings[POOL_SUBAGENTS_KEY];
69989
+ const rawAgents = isRecord$1(block) ? block[POOL_SUBAGENTS_AGENTS_KEY] : void 0;
69990
+ const agents = {};
69991
+ if (!isRecord$1(rawAgents)) return agents;
69992
+ for (const [name, raw] of Object.entries(rawAgents)) {
69993
+ if (isPrototypePollutionKey(name) || name === "general") continue;
69994
+ const result = PoolAgentSchema.safeParse(raw);
69995
+ if (!result.success) {
69996
+ logger?.warn(`Skipped Pool subagent ${quoteValueForWarning(name)} in ${filePath}: ${formatError(result.error)}`);
69997
+ continue;
69998
+ }
69999
+ agents[name] = result.data;
70000
+ }
70001
+ return agents;
70002
+ }
70003
+ /**
70004
+ * Pool subagents.
70005
+ *
70006
+ * Pool defines subagents inside its settings file — `.poolside/settings.yaml`
70007
+ * at project scope and `~/.config/poolside/settings.yaml` at user scope —
70008
+ * under `subagents.agents.<name>`, so every targeted rulesync subagent is
70009
+ * collapsed into that one file: the body becomes `instructions`, the shared
70010
+ * `description` is written as-is and the `pool:` frontmatter section supplies
70011
+ * `type` (default `in_process`) and the type-specific keys. rulesync owns the
70012
+ * `agents` map except for Pool's built-in `general` entry, which is carried
70013
+ * over untouched together with `subagents.default` and every other top-level
70014
+ * key of the file (`mcp_servers`, `tools`, `pool`, ...); the file is never
70015
+ * deleted.
70016
+ *
70017
+ * @see https://docs.poolside.ai/subagents
70018
+ * @see https://docs.poolside.ai/settings-file-reference
70019
+ */
70020
+ var PoolSubagent = class extends ToolSubagent {
70021
+ agents;
70022
+ logger;
70023
+ /**
70024
+ * The settings document to write, built lazily: the writer hands over the
70025
+ * existing file through `setFileContent` first, and a file that does not
70026
+ * exist yet gets the block merged into an empty document. Lazy so that a
70027
+ * `forDeletion` instance never consults the gateway.
70028
+ */
70029
+ mergedContent = null;
70030
+ constructor({ agents, logger, ...rest }) {
70031
+ super({
70032
+ ...rest,
70033
+ fileContent: ""
70034
+ });
70035
+ this.agents = agents;
70036
+ this.logger = logger;
70037
+ }
70038
+ getAgents() {
70039
+ return this.agents;
70040
+ }
70041
+ isDeletable() {
70042
+ return false;
70043
+ }
70044
+ shouldMergeExistingFileContent() {
70045
+ return true;
70046
+ }
70047
+ setFileContent(newFileContent) {
70048
+ this.mergedContent = this.mergeInto(newFileContent);
70049
+ }
70050
+ getFileContent() {
70051
+ if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
70052
+ return this.mergedContent;
70053
+ }
70054
+ /**
70055
+ * Rebuild the `subagents` block on top of an existing settings document:
70056
+ * `default` and any other sibling of `agents` are kept, the built-in
70057
+ * `general` agent is carried over, and every other agent is replaced by the
70058
+ * generated set — an agent removed from `.rulesync/subagents/` must not
70059
+ * survive here. A block left empty is retracted rather than written as `{}`.
70060
+ */
70061
+ mergeInto(existingContent) {
70062
+ const paths = {
70063
+ relativeDirPath: this.getRelativeDirPath(),
70064
+ relativeFilePath: this.getRelativeFilePath()
70065
+ };
70066
+ const filePath = (0, node_path.join)(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
70067
+ const existing = parsePoolSettings(existingContent, filePath);
70068
+ const existingBlock = isRecord$1(existing["subagents"]) ? existing[POOL_SUBAGENTS_KEY] : {};
70069
+ const block = {};
70070
+ for (const [key, value] of Object.entries(existingBlock)) if (!isPrototypePollutionKey(key) && key !== "agents") block[key] = value;
70071
+ const existingAgents = isRecord$1(existingBlock["agents"]) ? existingBlock[POOL_SUBAGENTS_AGENTS_KEY] : {};
70072
+ const general = existingAgents[POOL_GENERAL_AGENT_NAME];
70073
+ const agents = {
70074
+ ...general !== void 0 ? { [POOL_GENERAL_AGENT_NAME]: general } : {},
70075
+ ...this.agents
70076
+ };
70077
+ const dropped = Object.keys(existingAgents).filter((name) => name !== "general" && !Object.hasOwn(agents, name));
70078
+ if (dropped.length > 0) {
70079
+ const advice = this.global ? "hand-written agents belong in .rulesync/subagents/" : `hand-written agents belong in ${POOL_SETTINGS_LOCAL_FILE_NAME}`;
70080
+ 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}.`);
70081
+ }
70082
+ if (Object.keys(agents).length > 0) block[POOL_SUBAGENTS_AGENTS_KEY] = agents;
70083
+ return applySharedConfigPatch({
70084
+ fileKey: sharedConfigFileKey(paths),
70085
+ feature: "subagents",
70086
+ existingContent,
70087
+ patch: { [POOL_SUBAGENTS_KEY]: Object.keys(block).length > 0 ? block : void 0 },
70088
+ filePath,
70089
+ logger: this.logger
70090
+ });
70091
+ }
70092
+ /**
70093
+ * Every agent lives in the one settings file, which the MCP feature writes
70094
+ * too: naming the file here keeps the gitignore derivation from claiming the
70095
+ * whole directory and lets the shared-file derivation order the writers.
70096
+ */
70097
+ static getSettablePaths({ global = false } = {}) {
70098
+ return {
70099
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
70100
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
70101
+ };
70102
+ }
70103
+ /**
70104
+ * Map a single rulesync subagent to a Pool agent entry. The body becomes
70105
+ * `instructions` and the shared `description` is written as-is; the
70106
+ * optional `pool:` section supplies `type` and the type-specific keys and
70107
+ * may override both. Returns `null` for the reserved `general` name, which
70108
+ * Pool defines itself.
70109
+ */
70110
+ static toPoolAgent({ rulesyncSubagent, logger }) {
70111
+ const frontmatter = rulesyncSubagent.getFrontmatter();
70112
+ const name = frontmatter.name;
70113
+ if (name === "general") {
70114
+ 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.`);
70115
+ return null;
70116
+ }
70117
+ if (isPrototypePollutionKey(name)) {
70118
+ logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: unsafe name.`);
70119
+ return null;
70120
+ }
70121
+ const rawSection = isRecord$1(frontmatter.pool) ? frontmatter.pool : {};
70122
+ const result = PoolAgentSchema.safeParse(rawSection);
70123
+ if (!result.success) throw new Error(`Invalid "pool" section of subagent ${quoteValueForWarning(name)}: ${formatError(result.error)}`);
70124
+ const { type, description, instructions, ...rest } = result.data;
70125
+ const body = rulesyncSubagent.getBody().trim();
70126
+ const effectiveDescription = description ?? frontmatter.description;
70127
+ const effectiveInstructions = instructions ?? (body !== "" ? body : void 0);
70128
+ if (effectiveDescription === void 0 && rest.disabled !== true) logger?.warn(`Pool subagent ${quoteValueForWarning(name)} has no description; Pool requires one for every enabled custom agent.`);
70129
+ return {
70130
+ name,
70131
+ agent: {
70132
+ type: type ?? DEFAULT_POOL_AGENT_TYPE,
70133
+ ...effectiveDescription !== void 0 ? { description: effectiveDescription } : {},
70134
+ ...effectiveInstructions !== void 0 ? { instructions: effectiveInstructions } : {},
70135
+ ...rest
70136
+ }
70137
+ };
70138
+ }
70139
+ /**
70140
+ * Aggregate every targeted rulesync subagent into the one settings file.
70141
+ * Agents are keyed by their `name`; a repeated name keeps the last one, with
70142
+ * a warning, so the map stays what Pool would read.
70143
+ */
70144
+ static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true, global = false, logger }) {
70145
+ const agents = {};
70146
+ for (const rulesyncSubagent of rulesyncSubagents) {
70147
+ const converted = this.toPoolAgent({
70148
+ rulesyncSubagent,
70149
+ logger
70150
+ });
70151
+ if (converted === null) continue;
70152
+ if (Object.hasOwn(agents, converted.name)) logger?.warn(`Pool subagent ${quoteValueForWarning(converted.name)} is defined more than once; the last definition (${rulesyncSubagent.getRelativePathFromCwd()}) wins.`);
70153
+ agents[converted.name] = converted.agent;
70154
+ }
70155
+ return new this({
70156
+ outputRoot,
70157
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
70158
+ relativeFilePath: POOL_SETTINGS_FILE_NAME,
70159
+ agents,
70160
+ validate,
70161
+ global,
70162
+ logger
70163
+ });
70164
+ }
70165
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
70166
+ return this.fromRulesyncSubagents({
70167
+ outputRoot,
70168
+ rulesyncSubagents: [rulesyncSubagent],
70169
+ validate,
70170
+ global,
70171
+ logger
70172
+ });
70173
+ }
70174
+ /**
70175
+ * Convert every custom agent of the settings file back into an individual
70176
+ * rulesync subagent. `instructions` becomes the body, `description` the
70177
+ * shared field, and everything else — `type` only when it is not the
70178
+ * default — rides the `pool:` section so it survives the round-trip.
70179
+ */
70180
+ toRulesyncSubagents() {
70181
+ return Object.entries(this.agents).map(([name, agent]) => {
70182
+ const { type, description, instructions, ...rest } = agent;
70183
+ const poolSection = {
70184
+ ...type !== void 0 && type !== DEFAULT_POOL_AGENT_TYPE ? { type } : {},
70185
+ ...rest
70186
+ };
70187
+ const rulesyncFrontmatter = {
70188
+ targets: ["pool"],
70189
+ name,
70190
+ ...description !== void 0 ? { description } : {},
70191
+ ...Object.keys(poolSection).length > 0 ? { pool: poolSection } : {}
70192
+ };
70193
+ return new RulesyncSubagent({
70194
+ outputRoot: process.cwd(),
70195
+ frontmatter: rulesyncFrontmatter,
70196
+ body: instructions ?? "",
70197
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
70198
+ relativeFilePath: `${sanitizePoolAgentFileStem(name)}.md`,
70199
+ validate: true
70200
+ });
70201
+ });
70202
+ }
70203
+ toRulesyncSubagent() {
70204
+ const first = this.toRulesyncSubagents()[0];
70205
+ if (!first) throw new Error(`No custom subagents found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())} to convert.`);
70206
+ return first;
70207
+ }
70208
+ validate() {
70209
+ return {
70210
+ success: true,
70211
+ error: null
70212
+ };
70213
+ }
70214
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
70215
+ return this.isTargetedByRulesyncSubagentDefault({
70216
+ rulesyncSubagent,
70217
+ toolTarget: "pool"
70218
+ });
70219
+ }
70220
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, logger }) {
70221
+ const paths = this.getSettablePaths({ global });
70222
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
70223
+ const settings = parsePoolSettings(await readFileContentOrNull(filePath) ?? "", filePath);
70224
+ return new this({
70225
+ outputRoot,
70226
+ relativeDirPath: paths.relativeDirPath,
70227
+ relativeFilePath,
70228
+ agents: agentsOfSettings({
70229
+ settings,
70230
+ filePath,
70231
+ logger
70232
+ }),
70233
+ validate,
70234
+ global
70235
+ });
70236
+ }
70237
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
70238
+ return new this({
70239
+ outputRoot,
70240
+ relativeDirPath,
70241
+ relativeFilePath,
70242
+ agents: {},
70243
+ validate: false,
70244
+ global
70245
+ });
70246
+ }
70247
+ };
70248
+ //#endregion
68495
70249
  //#region src/features/subagents/reasonix-subagent.ts
68496
70250
  const ReasonixSubagentFrontmatterSchema = zod_mini.z.looseObject({
68497
70251
  name: zod_mini.z.string(),
@@ -69812,6 +71566,16 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
69812
71566
  filePattern: "*.md"
69813
71567
  }
69814
71568
  }],
71569
+ ["pool", {
71570
+ class: PoolSubagent,
71571
+ meta: {
71572
+ supportsProject: true,
71573
+ supportsSimulated: false,
71574
+ supportsGlobal: true,
71575
+ emitsEmptyAggregate: true,
71576
+ filePattern: POOL_SETTINGS_FILE_NAME
71577
+ }
71578
+ }],
69815
71579
  ["qwencode", {
69816
71580
  class: QwencodeSubagent,
69817
71581
  meta: {
@@ -69924,6 +71688,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
69924
71688
  toolTarget;
69925
71689
  global;
69926
71690
  getFactory;
71691
+ /**
71692
+ * Whether the last `loadRulesyncFiles` found a `subagents/` directory under
71693
+ * any input root. A project without one has not adopted the feature, so the
71694
+ * agents in its Pool settings are its own and must not be retracted.
71695
+ */
71696
+ rulesyncSourceDirFound = false;
69927
71697
  constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
69928
71698
  super({
69929
71699
  outputRoot,
@@ -69937,16 +71707,20 @@ var SubagentsProcessor = class extends FeatureProcessor {
69937
71707
  this.global = global;
69938
71708
  this.getFactory = getFactory;
69939
71709
  }
71710
+ emitsToolFilesForEmptySource() {
71711
+ return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
71712
+ }
69940
71713
  async convertRulesyncFilesToToolFiles(rulesyncFiles) {
69941
71714
  const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
69942
71715
  const factory = this.getFactory(this.toolTarget);
69943
71716
  const targeted = rulesyncSubagents.filter((rulesyncSubagent) => factory.class.isTargetedByRulesyncSubagent(rulesyncSubagent));
69944
71717
  if (factory.class.fromRulesyncSubagents) {
69945
- if (targeted.length === 0) return [];
71718
+ if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
69946
71719
  const toolSubagents = factory.class.fromRulesyncSubagents({
69947
71720
  outputRoot: this.outputRoot,
69948
71721
  rulesyncSubagents: targeted,
69949
- global: this.global
71722
+ global: this.global,
71723
+ logger: this.logger
69950
71724
  });
69951
71725
  return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
69952
71726
  }
@@ -70002,6 +71776,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
70002
71776
  this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
70003
71777
  return [];
70004
71778
  }
71779
+ this.rulesyncSourceDirFound = true;
70005
71780
  const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
70006
71781
  if (mdFiles.length === 0) {
70007
71782
  this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
@@ -70042,6 +71817,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
70042
71817
  * earlier root's copy.
70043
71818
  */
70044
71819
  async loadRulesyncFiles() {
71820
+ this.rulesyncSourceDirFound = false;
70045
71821
  const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
70046
71822
  perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
70047
71823
  identity: (subagent) => subagent.getRelativeFilePath(),
@@ -70104,7 +71880,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
70104
71880
  outputRoot: rootOutputRoot,
70105
71881
  relativeDirPath: dirPath,
70106
71882
  relativeFilePath: toRelativeFilePath(path),
70107
- global: this.global
71883
+ global: this.global,
71884
+ logger: this.logger
70108
71885
  })));
70109
71886
  toolSubagents.push(...this.claimStandaloneSubagents({
70110
71887
  loaded,
@@ -74026,22 +75803,30 @@ var DevinRule = class DevinRule extends ToolRule {
74026
75803
  }
74027
75804
  };
74028
75805
  //#endregion
74029
- //#region src/features/rules/dsh-rule.ts
75806
+ //#region src/features/rules/nested-agentsmd-rule.ts
74030
75807
  /**
74031
- * DeepSeek Harness (`dsh`) rules.
75808
+ * Shared adapter for the targets that read instructions exactly the way the
75809
+ * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
75810
+ * project root, nested per-directory `AGENTS.md` files loaded only while working
75811
+ * under that directory, and a single personal root file under a tool-specific
75812
+ * home directory. Such a target has no modular non-root instruction directory,
75813
+ * so every other non-root rule folds into the root file.
74032
75814
  *
74033
- * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
74034
- * files — the project chain `dsh-agent-instructions` loads from the `.git`
74035
- * root down to the session cwd. Global scope writes the user-global
74036
- * `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
74037
- * single root file with no local overlay, so non-root rules fold into it.
75815
+ * Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
75816
+ * directory and their tool target through {@link getFamily}; the vendor docs
75817
+ * describing each one's discovery walk live on the subclass.
74038
75818
  *
74039
- * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
75819
+ * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
75820
+ * same file name, same exclusions, import-only, project scope — because it
75821
+ * discovers literally the same files.
74040
75822
  */
74041
- var DshRule = class DshRule extends ToolRule {
75823
+ var NestedAgentsmdRule = class extends ToolRule {
75824
+ static getFamily() {
75825
+ throw new Error("Please implement this method in the subclass.");
75826
+ }
74042
75827
  static getSettablePaths({ global = false } = {}) {
74043
75828
  if (global) return { root: {
74044
- relativeDirPath: DSH_DIR,
75829
+ relativeDirPath: this.getFamily().globalDir,
74045
75830
  relativeFilePath: AGENTSMD_RULE_FILE_NAME
74046
75831
  } };
74047
75832
  return { root: {
@@ -74049,18 +75834,6 @@ var DshRule = class DshRule extends ToolRule {
74049
75834
  relativeFilePath: AGENTSMD_RULE_FILE_NAME
74050
75835
  } };
74051
75836
  }
74052
- /**
74053
- * DeepSeek Harness loads the project chain of `AGENTS.md` files from the
74054
- * project root (the nearest ancestor holding `.git`) down through the session
74055
- * cwd, broad to specific, so a per-directory file applies only while working
74056
- * under that directory. Nested files are therefore a real scoping surface,
74057
- * not just the root file's overflow.
74058
- *
74059
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
74060
- * name, same exclusions, import-only, project scope — because it discovers
74061
- * literally the same files.
74062
- * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
74063
- */
74064
75837
  static getNestedFilePatterns() {
74065
75838
  return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
74066
75839
  }
@@ -74075,7 +75848,7 @@ var DshRule = class DshRule extends ToolRule {
74075
75848
  const { root } = this.getSettablePaths({ global });
74076
75849
  if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
74077
75850
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
74078
- return new DshRule({
75851
+ return new this({
74079
75852
  outputRoot,
74080
75853
  relativeDirPath: overrideDirPath,
74081
75854
  relativeFilePath: AGENTSMD_RULE_FILE_NAME,
@@ -74085,7 +75858,7 @@ var DshRule = class DshRule extends ToolRule {
74085
75858
  });
74086
75859
  }
74087
75860
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
74088
- return new DshRule({
75861
+ return new this({
74089
75862
  outputRoot,
74090
75863
  relativeDirPath: root.relativeDirPath,
74091
75864
  relativeFilePath: root.relativeFilePath,
@@ -74099,7 +75872,7 @@ var DshRule = class DshRule extends ToolRule {
74099
75872
  const frontmatter = rulesyncRule.getFrontmatter();
74100
75873
  const isRoot = frontmatter.root ?? false;
74101
75874
  const subprojectPath = frontmatter.agentsmd?.subprojectPath;
74102
- if (!global && !isRoot && subprojectPath) return new DshRule({
75875
+ if (!global && !isRoot && subprojectPath) return new this({
74103
75876
  outputRoot,
74104
75877
  relativeDirPath: (0, node_path.join)(subprojectPath),
74105
75878
  relativeFilePath: AGENTSMD_RULE_FILE_NAME,
@@ -74107,7 +75880,7 @@ var DshRule = class DshRule extends ToolRule {
74107
75880
  validate,
74108
75881
  root: false
74109
75882
  });
74110
- return new DshRule({
75883
+ return new this({
74111
75884
  outputRoot,
74112
75885
  relativeDirPath: root.relativeDirPath,
74113
75886
  relativeFilePath: root.relativeFilePath,
@@ -74128,23 +75901,47 @@ var DshRule = class DshRule extends ToolRule {
74128
75901
  };
74129
75902
  }
74130
75903
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
74131
- return new DshRule({
75904
+ const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
75905
+ return new this({
74132
75906
  outputRoot,
74133
75907
  relativeDirPath,
74134
75908
  relativeFilePath,
74135
75909
  fileContent: "",
74136
75910
  validate: false,
74137
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".dsh")
75911
+ root: isRoot
74138
75912
  });
74139
75913
  }
74140
75914
  static isTargetedByRulesyncRule(rulesyncRule) {
74141
75915
  return this.isTargetedByRulesyncRuleDefault({
74142
75916
  rulesyncRule,
74143
- toolTarget: "dsh"
75917
+ toolTarget: this.getFamily().toolTarget
74144
75918
  });
74145
75919
  }
74146
75920
  };
74147
75921
  //#endregion
75922
+ //#region src/features/rules/dsh-rule.ts
75923
+ /**
75924
+ * DeepSeek Harness (`dsh`) rules.
75925
+ *
75926
+ * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
75927
+ * files — the project chain `dsh-agent-instructions` loads from the `.git`
75928
+ * root down to the session cwd, broad to specific, so a per-directory file
75929
+ * applies only while working under that directory. Global scope writes the
75930
+ * user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
75931
+ * reads as a single root file with no `.local.md` overlay, so non-root rules
75932
+ * fold into it.
75933
+ *
75934
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
75935
+ */
75936
+ var DshRule = class extends NestedAgentsmdRule {
75937
+ static getFamily() {
75938
+ return {
75939
+ globalDir: DSH_DIR,
75940
+ toolTarget: "dsh"
75941
+ };
75942
+ }
75943
+ };
75944
+ //#endregion
74148
75945
  //#region src/features/rules/factorydroid-rule.ts
74149
75946
  /**
74150
75947
  * Rule generator for Factory Droid.
@@ -75538,110 +77335,24 @@ var PiRule = class PiRule extends ToolRule {
75538
77335
  };
75539
77336
  //#endregion
75540
77337
  //#region src/features/rules/pool-rule.ts
75541
- var PoolRule = class PoolRule extends ToolRule {
75542
- static getSettablePaths({ global = false } = {}) {
75543
- if (global) return { root: {
75544
- relativeDirPath: POOL_GLOBAL_DIR,
75545
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
75546
- } };
75547
- return { root: {
75548
- relativeDirPath: ".",
75549
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
75550
- } };
75551
- }
75552
- /**
75553
- * Pool reads personal, project, and directory-level `AGENTS.md` files: inside
75554
- * a git repository it loads every `AGENTS.md` from the repository root down
75555
- * through the working directory (deeper files take precedence), skipping
75556
- * ignored directories. Nested files are therefore a real scoping surface, not
75557
- * just the root file's overflow.
75558
- *
75559
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
75560
- * name, same exclusions, import-only, project scope — because it discovers
75561
- * literally the same files.
75562
- * @see https://docs.poolside.ai/agent-instructions
75563
- */
75564
- static getNestedFilePatterns() {
75565
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
75566
- }
75567
- /**
75568
- * The subproject directory this rule scopes, or `undefined` for the root file
75569
- * (project or global).
75570
- */
75571
- getSubprojectPath() {
75572
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
75573
- }
75574
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
75575
- const { root } = this.getSettablePaths({ global });
75576
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
75577
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
75578
- return new PoolRule({
75579
- outputRoot,
75580
- relativeDirPath: overrideDirPath,
75581
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75582
- fileContent,
75583
- validate,
75584
- root: false
75585
- });
75586
- }
75587
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
75588
- return new PoolRule({
75589
- outputRoot,
75590
- relativeDirPath: root.relativeDirPath,
75591
- relativeFilePath: root.relativeFilePath,
75592
- fileContent,
75593
- validate,
75594
- root: true
75595
- });
75596
- }
75597
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
75598
- const { root } = this.getSettablePaths({ global });
75599
- const frontmatter = rulesyncRule.getFrontmatter();
75600
- const isRoot = frontmatter.root ?? false;
75601
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
75602
- if (!global && !isRoot && subprojectPath) return new PoolRule({
75603
- outputRoot,
75604
- relativeDirPath: (0, node_path.join)(subprojectPath),
75605
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75606
- fileContent: rulesyncRule.getBody(),
75607
- validate,
75608
- root: false
75609
- });
75610
- return new PoolRule({
75611
- outputRoot,
75612
- relativeDirPath: root.relativeDirPath,
75613
- relativeFilePath: root.relativeFilePath,
75614
- fileContent: rulesyncRule.getBody(),
75615
- validate,
75616
- root: isRoot
75617
- });
75618
- }
75619
- toRulesyncRule() {
75620
- const subprojectPath = this.getSubprojectPath();
75621
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
75622
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
75623
- }
75624
- validate() {
77338
+ /**
77339
+ * Pool (Poolside) rules.
77340
+ *
77341
+ * Pool reads personal, project, and directory-level `AGENTS.md` files: inside
77342
+ * a git repository it loads every `AGENTS.md` from the repository root down
77343
+ * through the working directory (deeper files take precedence), skipping
77344
+ * ignored directories. Nested files are therefore a real scoping surface, not
77345
+ * just the root file's overflow. The personal file sits below both at
77346
+ * `~/.config/poolside/AGENTS.md`.
77347
+ *
77348
+ * @see https://docs.poolside.ai/agent-instructions
77349
+ */
77350
+ var PoolRule = class extends NestedAgentsmdRule {
77351
+ static getFamily() {
75625
77352
  return {
75626
- success: true,
75627
- error: null
75628
- };
75629
- }
75630
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
75631
- return new PoolRule({
75632
- outputRoot,
75633
- relativeDirPath,
75634
- relativeFilePath,
75635
- fileContent: "",
75636
- validate: false,
75637
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === POOL_GLOBAL_DIR)
75638
- });
75639
- }
75640
- static isTargetedByRulesyncRule(rulesyncRule) {
75641
- return this.isTargetedByRulesyncRuleDefault({
75642
- rulesyncRule,
77353
+ globalDir: POOL_GLOBAL_DIR,
75643
77354
  toolTarget: "pool"
75644
- });
77355
+ };
75645
77356
  }
75646
77357
  };
75647
77358
  //#endregion
@@ -76605,110 +78316,23 @@ var TaktRule = class TaktRule extends ToolRule {
76605
78316
  //#endregion
76606
78317
  //#region src/features/rules/vibe-rule.ts
76607
78318
  const VIBE_GLOBAL_DIR = ".vibe";
76608
- var VibeRule = class VibeRule extends ToolRule {
76609
- static getSettablePaths({ global = false } = {}) {
76610
- if (global) return { root: {
76611
- relativeDirPath: VIBE_GLOBAL_DIR,
76612
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76613
- } };
76614
- return { root: {
76615
- relativeDirPath: ".",
76616
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76617
- } };
76618
- }
76619
- /**
76620
- * Vibe's harness manager walks the directories between the workspace root and
76621
- * the file being read and loads every `AGENTS.md` it finds along the way
76622
- * (`find_subdirectory_agents_md`), injecting the result into the `read_file`
76623
- * tool's output. Nested files are therefore a real scoping surface, not just
76624
- * the root file's overflow.
76625
- *
76626
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
76627
- * name, same exclusions, import-only, project scope — because it discovers
76628
- * literally the same files.
76629
- * @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
76630
- */
76631
- static getNestedFilePatterns() {
76632
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
76633
- }
76634
- /**
76635
- * The subproject directory this rule scopes, or `undefined` for the root file
76636
- * (project or global).
76637
- */
76638
- getSubprojectPath() {
76639
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
76640
- }
76641
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
76642
- const { root } = this.getSettablePaths({ global });
76643
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
76644
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
76645
- return new VibeRule({
76646
- outputRoot,
76647
- relativeDirPath: overrideDirPath,
76648
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76649
- fileContent,
76650
- validate,
76651
- root: false
76652
- });
76653
- }
76654
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
76655
- return new VibeRule({
76656
- outputRoot,
76657
- relativeDirPath: root.relativeDirPath,
76658
- relativeFilePath: root.relativeFilePath,
76659
- fileContent,
76660
- validate,
76661
- root: true
76662
- });
76663
- }
76664
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
76665
- const { root } = this.getSettablePaths({ global });
76666
- const frontmatter = rulesyncRule.getFrontmatter();
76667
- const isRoot = frontmatter.root ?? false;
76668
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
76669
- if (!global && !isRoot && subprojectPath) return new VibeRule({
76670
- outputRoot,
76671
- relativeDirPath: (0, node_path.join)(subprojectPath),
76672
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76673
- fileContent: rulesyncRule.getBody(),
76674
- validate,
76675
- root: false
76676
- });
76677
- return new VibeRule({
76678
- outputRoot,
76679
- relativeDirPath: root.relativeDirPath,
76680
- relativeFilePath: root.relativeFilePath,
76681
- fileContent: rulesyncRule.getBody(),
76682
- validate,
76683
- root: isRoot
76684
- });
76685
- }
76686
- toRulesyncRule() {
76687
- const subprojectPath = this.getSubprojectPath();
76688
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
76689
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
76690
- }
76691
- validate() {
78319
+ /**
78320
+ * Mistral Vibe rules.
78321
+ *
78322
+ * Vibe's harness manager walks the directories between the workspace root and
78323
+ * the file being read and loads every `AGENTS.md` it finds along the way
78324
+ * (`find_subdirectory_agents_md`), injecting the result into the `read_file`
78325
+ * tool's output. Nested files are therefore a real scoping surface, not just
78326
+ * the root file's overflow. The personal file is `~/.vibe/AGENTS.md`.
78327
+ *
78328
+ * @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
78329
+ */
78330
+ var VibeRule = class extends NestedAgentsmdRule {
78331
+ static getFamily() {
76692
78332
  return {
76693
- success: true,
76694
- error: null
76695
- };
76696
- }
76697
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
76698
- return new VibeRule({
76699
- outputRoot,
76700
- relativeDirPath,
76701
- relativeFilePath,
76702
- fileContent: "",
76703
- validate: false,
76704
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".vibe")
76705
- });
76706
- }
76707
- static isTargetedByRulesyncRule(rulesyncRule) {
76708
- return this.isTargetedByRulesyncRuleDefault({
76709
- rulesyncRule,
78333
+ globalDir: VIBE_GLOBAL_DIR,
76710
78334
  toolTarget: "vibe"
76711
- });
78335
+ };
76712
78336
  }
76713
78337
  };
76714
78338
  //#endregion
@@ -78915,6 +80539,16 @@ async function activateHermesProjectPlugins({ pluginNames, dryRun, logger }) {
78915
80539
  relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
78916
80540
  });
78917
80541
  const configPath = (0, node_path.join)(configRoot, relativeConfigPath);
80542
+ if (await refusesWriteOutsideRoot({
80543
+ logger,
80544
+ rootPath: configRoot,
80545
+ targetPath: configPath
80546
+ })) return {
80547
+ count: 0,
80548
+ paths: [],
80549
+ hasDiff: false,
80550
+ sourceLoadFailed: false
80551
+ };
78918
80552
  const expectedConfig = mergeEnabledPlugins({
78919
80553
  existingContent: await readFileContentOrNull(configPath) ?? "",
78920
80554
  filePath: configPath,
@@ -79382,7 +81016,8 @@ async function processEmptyFeatureGeneration(params) {
79382
81016
  */
79383
81017
  async function processFeatureWithRulesyncFiles(params) {
79384
81018
  const { config, processor, rulesyncFiles, sweepPlan, skipFilePaths, onToolFiles } = params;
79385
- if (rulesyncFiles.length === 0) return processEmptyFeatureGeneration({
81019
+ const retractsForEmptySource = processor.emitsToolFilesForEmptySource() && !processor.hasRulesyncSourceLoadFailure();
81020
+ if (rulesyncFiles.length === 0 && !retractsForEmptySource) return processEmptyFeatureGeneration({
79386
81021
  config,
79387
81022
  processor,
79388
81023
  sweepPlan,