rulesync 16.38.0 → 16.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import { ZodError } from "zod";
2
2
  import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
3
3
  import { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat, writeFile } from "node:fs/promises";
4
- import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
5
- import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse, parseTree, printParseErrorCode } from "jsonc-parser";
4
+ import path, { basename, dirname, extname, isAbsolute, join, normalize, parse, posix, relative, resolve, sep, win32 } from "node:path";
5
+ import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse as parse$1, parseTree, printParseErrorCode } from "jsonc-parser";
6
6
  import { constants } from "node:fs";
7
7
  import os from "node:os";
8
8
  import { intersection, kebabCase, uniq } from "es-toolkit";
@@ -14,7 +14,7 @@ import { YAMLException, dump, load } from "js-yaml";
14
14
  import { omit } from "es-toolkit/object";
15
15
  import { createHash } from "node:crypto";
16
16
  import * as smolToml from "smol-toml";
17
- import { parse as parse$1, stringify } from "smol-toml";
17
+ import { parse as parse$2, stringify } from "smol-toml";
18
18
  import { encode } from "@toon-format/toon";
19
19
  //#region src/utils/control-characters.ts
20
20
  /**
@@ -671,6 +671,7 @@ const subagentsProcessorToolTargetTuple = [
671
671
  "kiro-cli",
672
672
  "kiro-ide",
673
673
  "opencode",
674
+ "pool",
674
675
  "qwencode",
675
676
  "reasonix",
676
677
  "roo",
@@ -752,6 +753,7 @@ const hooksProcessorToolTargetTuple = [
752
753
  "crush",
753
754
  "opencode",
754
755
  "pi",
756
+ "pool",
755
757
  "factorydroid",
756
758
  "goose",
757
759
  "hermesagent",
@@ -799,6 +801,7 @@ const permissionsProcessorToolTargetTuple = [
799
801
  "kiro-ide",
800
802
  "opencode",
801
803
  "pi",
804
+ "pool",
802
805
  "qwencode",
803
806
  "reasonix",
804
807
  "roo",
@@ -1355,6 +1358,111 @@ async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
1355
1358
  }));
1356
1359
  }
1357
1360
  /**
1361
+ * How many links {@link writeLandingPath} follows before it treats the chain as
1362
+ * a cycle it cannot write through. Linux itself gives up after 40.
1363
+ */
1364
+ const MAX_LINK_HOPS = 40;
1365
+ /**
1366
+ * Whether `error` is the ENOTDIR a lookup through a file raises. On the way to
1367
+ * a write it means the same as a missing entry: nothing at or below the path
1368
+ * exists, so nothing there can be a link.
1369
+ */
1370
+ function isNotADirectoryError(error) {
1371
+ return someErrorInChain(error, (candidate) => "code" in candidate && candidate.code === "ENOTDIR");
1372
+ }
1373
+ /**
1374
+ * Where a write to `targetPath` lands once the OS has followed every link on
1375
+ * the way there, or `null` when the links form a cycle no write can get
1376
+ * through. The target itself usually does not exist yet, so the walk goes
1377
+ * segment by segment from the filesystem root: an existing segment that is a
1378
+ * link is replaced by what it resolves to, and a dangling link — one whose
1379
+ * target does not exist yet, which is exactly the shape of a link planted at
1380
+ * `AGENTS.md` pointing at a `~/.zshenv` that is still to be created — has its
1381
+ * target spliced into the walk in its place, so the segments of that target
1382
+ * are looked up one by one too. Once a segment is missing, nothing below it
1383
+ * exists, and the rest of the path is where the write's `mkdir -p` will put it.
1384
+ *
1385
+ * Walking segment by segment is what keeps a `..` honest. A link target is a
1386
+ * path the OS resolves left to right, so in `sub/../victim` the `..` climbs out
1387
+ * of wherever `sub` leads, not out of the directory `sub` is spelled in; a
1388
+ * lexical `resolve` folds `sub/..` away before `sub` is ever looked at, which
1389
+ * is how a link chain could be spelled to pass the check and still land
1390
+ * outside the root.
1391
+ */
1392
+ async function writeLandingPath(targetPath) {
1393
+ const start = splitAbsolutePath(isAbsolute(targetPath) ? targetPath : resolve(targetPath));
1394
+ let current = start.root;
1395
+ let pending = start.segments;
1396
+ let exists = true;
1397
+ let linkHops = 0;
1398
+ while (pending.length > 0) {
1399
+ const segment = pending.shift();
1400
+ if (segment === void 0 || segment === "" || segment === ".") continue;
1401
+ if (segment === "..") {
1402
+ current = dirname(current);
1403
+ continue;
1404
+ }
1405
+ current = join(current, segment);
1406
+ if (!exists) continue;
1407
+ let stats;
1408
+ try {
1409
+ stats = await lstat(current);
1410
+ } catch (error) {
1411
+ if (!isFileNotFoundError(error) && !isNotADirectoryError(error)) throw error;
1412
+ exists = false;
1413
+ continue;
1414
+ }
1415
+ if (!stats.isSymbolicLink()) continue;
1416
+ linkHops += 1;
1417
+ if (linkHops > MAX_LINK_HOPS) return null;
1418
+ try {
1419
+ current = await realpath(current);
1420
+ continue;
1421
+ } catch {}
1422
+ const linkTarget = await readlink(current);
1423
+ if (isAbsolute(linkTarget)) {
1424
+ const target = splitAbsolutePath(linkTarget);
1425
+ current = target.root;
1426
+ pending = [...target.segments, ...pending];
1427
+ } else {
1428
+ current = dirname(current);
1429
+ pending = [...splitPathSegments(linkTarget), ...pending];
1430
+ }
1431
+ }
1432
+ return current;
1433
+ }
1434
+ /**
1435
+ * Split an absolute path into its root and its raw segments, without folding a
1436
+ * `..` away: {@link writeLandingPath} has to see every `..` where it is spelled.
1437
+ */
1438
+ function splitAbsolutePath(absolutePath) {
1439
+ const root = parse(absolutePath).root;
1440
+ return {
1441
+ root,
1442
+ segments: splitPathSegments(absolutePath.slice(root.length))
1443
+ };
1444
+ }
1445
+ /**
1446
+ * Whether writing `targetPath` would land outside `rootPath` once every link on
1447
+ * the way there is resolved. Both sides are taken to where a write to them
1448
+ * would land (see {@link writeLandingPath}), so a root that does not fully
1449
+ * exist yet is judged the same way as the target below it.
1450
+ *
1451
+ * A link that stays inside the root passes. That is what sets this apart from
1452
+ * the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
1453
+ * dotfiles checkout linked from inside the home directory is a common shape for
1454
+ * a global write target, and refusing it would refuse `--global` outright.
1455
+ *
1456
+ * A link chain that cycles is reported as an escape: the write could not get
1457
+ * through it, and refusing it is the answer that never writes somewhere
1458
+ * unexpected.
1459
+ */
1460
+ async function writablePathEscapesRoot({ rootPath, targetPath }) {
1461
+ const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
1462
+ if (rootLanding === null || targetLanding === null) return true;
1463
+ return pathEscapesRoot(relative(rootLanding, targetLanding));
1464
+ }
1465
+ /**
1358
1466
  * How many trailing segments `filePath` and `identity` have in common.
1359
1467
  *
1360
1468
  * A path that walked through no link at all shares all of its own segments with
@@ -2936,7 +3044,7 @@ const getDefaults = () => ({
2936
3044
  const loadConfigFromFile = async (filePath) => {
2937
3045
  if (!await fileExists(filePath)) return {};
2938
3046
  const fileContent = await readFileContent(filePath);
2939
- const jsonData = parse(fileContent);
3047
+ const jsonData = parse$1(fileContent);
2940
3048
  const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
2941
3049
  assertTargetsFeaturesExclusive({
2942
3050
  targets: configParams.targets,
@@ -4654,6 +4762,34 @@ const VIBE_HOOK_EVENTS = [
4654
4762
  "stop"
4655
4763
  ];
4656
4764
  /**
4765
+ * Hook events supported by Pool.
4766
+ *
4767
+ * Pool's `hooks` settings block fires six events, spelled the Claude Code
4768
+ * way: `PreToolUse`, `PostToolUse`, `UserPromptSubmit` (← `beforeSubmitPrompt`),
4769
+ * `PreCompact`, `SessionStart` and `Stop`. Every entry carries a `matcher`
4770
+ * (required by Pool's schema; `""`/`*` match any tool, a bare name is an exact
4771
+ * match, `a|b` lists names, anything else is a regular expression), which only
4772
+ * `PreToolUse`/`PostToolUse` test against the tool name.
4773
+ * @see https://docs.poolside.ai/hooks
4774
+ */
4775
+ const POOL_HOOK_EVENTS = [
4776
+ "preToolUse",
4777
+ "postToolUse",
4778
+ "beforeSubmitPrompt",
4779
+ "preCompact",
4780
+ "sessionStart",
4781
+ "stop"
4782
+ ];
4783
+ const CANONICAL_TO_POOL_EVENT_NAMES = {
4784
+ preToolUse: "PreToolUse",
4785
+ postToolUse: "PostToolUse",
4786
+ beforeSubmitPrompt: "UserPromptSubmit",
4787
+ preCompact: "PreCompact",
4788
+ sessionStart: "SessionStart",
4789
+ stop: "Stop"
4790
+ };
4791
+ const POOL_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_EVENT_NAMES).map(([k, v]) => [v, k]));
4792
+ /**
4657
4793
  * Hook events supported by Crush.
4658
4794
  *
4659
4795
  * Crush's `hooks` config block currently fires a single event, `PreToolUse`
@@ -5248,6 +5384,7 @@ const HooksConfigSchema = z.looseObject({
5248
5384
  "kimi-code": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5249
5385
  zcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5250
5386
  crush: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5387
+ pool: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5251
5388
  bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5252
5389
  cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5253
5390
  commandcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
@@ -5922,7 +6059,7 @@ function parseJsonc(content) {
5922
6059
  */
5923
6060
  function parseStrict(content) {
5924
6061
  const errors = [];
5925
- const result = parse(content, errors, {
6062
+ const result = parse$1(content, errors, {
5926
6063
  allowTrailingComma: true,
5927
6064
  disallowComments: false
5928
6065
  });
@@ -7976,6 +8113,7 @@ const PermissionsConfigSchema = z.looseObject({
7976
8113
  copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
7977
8114
  crush: z.optional(CanonicalPermissionsOverrideSchema),
7978
8115
  goose: z.optional(CanonicalPermissionsOverrideSchema),
8116
+ pool: z.optional(CanonicalPermissionsOverrideSchema),
7979
8117
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
7980
8118
  "kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
7981
8119
  roo: z.optional(CanonicalPermissionsOverrideSchema),
@@ -9818,6 +9956,22 @@ allowedMcpServers: z.optional(z.array(z.string())) })),
9818
9956
  injectAgentsMd: z.optional(z.boolean()),
9819
9957
  mcpServers: z.optional(z.array(z.string()))
9820
9958
  })),
9959
+ pool: z.optional(z.looseObject({
9960
+ type: z.optional(z.enum([
9961
+ "in_process",
9962
+ "command",
9963
+ "agent_server"
9964
+ ])),
9965
+ description: z.optional(z.string()),
9966
+ instructions: z.optional(z.string()),
9967
+ command: z.optional(z.string()),
9968
+ args: z.optional(z.array(z.string())),
9969
+ env: z.optional(z.record(z.string(), z.string())),
9970
+ agent_server: z.optional(z.string()),
9971
+ session_config_options: z.optional(z.record(z.string(), z.unknown())),
9972
+ inherit_agent_config: z.optional(z.boolean()),
9973
+ disabled: z.optional(z.boolean())
9974
+ })),
9821
9975
  vibe: z.optional(z.looseObject({
9822
9976
  agent_type: z.optional(z.enum(["agent", "subagent"])),
9823
9977
  display_name: z.optional(z.string()),
@@ -10179,7 +10333,7 @@ function tryParseStructured({ filePath, content }) {
10179
10333
  case ".json":
10180
10334
  case ".jsonc": {
10181
10335
  const errors = [];
10182
- const value = parse(content, errors);
10336
+ const value = parse$1(content, errors);
10183
10337
  return errors.length > 0 ? PARSE_FAILED : {
10184
10338
  ok: true,
10185
10339
  value
@@ -10405,6 +10559,17 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
10405
10559
  this.logger = logger;
10406
10560
  }
10407
10561
  /**
10562
+ * Whether `convertRulesyncFilesToToolFiles` still yields tool files for an
10563
+ * empty source list. A processor that aggregates into a shared file it may
10564
+ * never delete (Pool's `settings.yaml` subagents) answers true so a run
10565
+ * whose last source file is gone retracts what an earlier run generated;
10566
+ * the default keeps a run with no source from writing anything. Consulted
10567
+ * after `loadRulesyncFiles`, so the answer may depend on what that load saw.
10568
+ */
10569
+ emitsToolFilesForEmptySource() {
10570
+ return false;
10571
+ }
10572
+ /**
10408
10573
  * Return tool targets that this feature supports.
10409
10574
  */
10410
10575
  static getToolTargets(_params = {}) {
@@ -10419,6 +10584,11 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
10419
10584
  const changedPaths = [];
10420
10585
  for (const aiFile of aiFiles) {
10421
10586
  const filePath = aiFile.getFilePath();
10587
+ if (await refusesWriteOutsideRoot({
10588
+ logger: this.logger,
10589
+ rootPath: aiFile.getOutputRoot(),
10590
+ targetPath: filePath
10591
+ })) continue;
10422
10592
  const existingFileContent = await readFileContentOrNull(filePath);
10423
10593
  if (existingFileContent !== null && aiFile.shouldMergeExistingFileContent()) aiFile.setFileContent(existingFileContent);
10424
10594
  const contentWithNewline = addTrailingNewline(aiFile.getFileContent());
@@ -10551,6 +10721,37 @@ function mergeByIdentity({ perRoot, identity }) {
10551
10721
  return order.map((key) => winnerByKey.get(key));
10552
10722
  }
10553
10723
  /**
10724
+ * Whether a write to `targetPath` has to be refused because a link on the way
10725
+ * there leads out of `rootPath`, warning about the refusal.
10726
+ *
10727
+ * A checked-out repository can carry a symbolic link at an output path
10728
+ * (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`), and a write
10729
+ * through it would land wherever the link points. Every writer that lets the
10730
+ * OS follow links — file, directory, and config-rewrite alike — asks this first
10731
+ * so that one warning describes the one behavior.
10732
+ */
10733
+ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
10734
+ if (!await writablePathEscapesRoot({
10735
+ rootPath,
10736
+ targetPath
10737
+ })) return false;
10738
+ logger.warn(`Refusing to write ${quoteForLog(targetPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
10739
+ return true;
10740
+ }
10741
+ /**
10742
+ * {@link refusesWriteOutsideRoot} over every path a single unit of output
10743
+ * writes, stopping at the first refusal: a directory written as one unit is
10744
+ * held back whole when any one file in it would land outside the root.
10745
+ */
10746
+ async function refusesAnyWriteOutsideRoot({ logger, rootPath, targetPaths }) {
10747
+ for (const targetPath of targetPaths) if (await refusesWriteOutsideRoot({
10748
+ logger,
10749
+ rootPath,
10750
+ targetPath
10751
+ })) return true;
10752
+ return false;
10753
+ }
10754
+ /**
10554
10755
  * The key two spellings share when a case-insensitive filesystem would give
10555
10756
  * them one file. `toLowerCase()` is locale-independent (unlike
10556
10757
  * `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
@@ -12224,16 +12425,16 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
12224
12425
  let parsed;
12225
12426
  try {
12226
12427
  if (format === "yaml") parsed = loadYaml(fileContent);
12227
- else if (format === "toml") parsed = parse$1(fileContent);
12428
+ else if (format === "toml") parsed = parse$2(fileContent);
12228
12429
  else if (format === "json") parsed = JSON.parse(fileContent);
12229
12430
  else if (jsoncParseErrors === "error") {
12230
12431
  const errors = [];
12231
- parsed = parse(fileContent, errors, { allowTrailingComma: true });
12432
+ parsed = parse$1(fileContent, errors, { allowTrailingComma: true });
12232
12433
  if (errors.length > 0) {
12233
12434
  const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
12234
12435
  throw new Error(details);
12235
12436
  }
12236
- } else parsed = parse(fileContent);
12437
+ } else parsed = parse$1(fileContent);
12237
12438
  } catch (error) {
12238
12439
  throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
12239
12440
  }
@@ -13015,15 +13216,33 @@ const KIMI_CODE_CONFIG_DECLARATION = {
13015
13216
  * Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
13016
13217
  * the user's primary Pool settings file, so an unparseable root fails closed
13017
13218
  * rather than being replaced with generated output. `mcp_servers` is owned as
13018
- * a whole key because the MCP writer emits the complete server map.
13219
+ * a whole key because the MCP writer emits the complete server map. The
13220
+ * permissions writer deep-merges: it rewrites the `allow`/`deny` lists of the
13221
+ * `tools.<name>` blocks it manages and the `paths` lists, leaving each tool's
13222
+ * `disabled` flag, unmanaged tools and every other key in place. `hooks` is
13223
+ * owned as a whole key because the hooks writer emits every event list.
13224
+ * `subagents` is owned as a whole key too: its writer recomputes the block
13225
+ * from the existing file (`default` and the built-in `general` agent carried
13226
+ * over, every other agent replaced) before applying the patch.
13019
13227
  */
13020
13228
  const POOL_SETTINGS_DECLARATION = {
13021
13229
  format: "yaml",
13022
13230
  invalidRootPolicy: "error",
13023
- features: { mcp: {
13024
- kind: "replace-owned-keys",
13025
- ownedKeys: ["mcp_servers"]
13026
- } }
13231
+ features: {
13232
+ mcp: {
13233
+ kind: "replace-owned-keys",
13234
+ ownedKeys: ["mcp_servers"]
13235
+ },
13236
+ permissions: { kind: "deep-merge" },
13237
+ hooks: {
13238
+ kind: "replace-owned-keys",
13239
+ ownedKeys: ["hooks"]
13240
+ },
13241
+ subagents: {
13242
+ kind: "replace-owned-keys",
13243
+ ownedKeys: ["subagents"]
13244
+ }
13245
+ }
13027
13246
  };
13028
13247
  /**
13029
13248
  * ZCode's settings file, which also carries model/theme/permission keys
@@ -19301,7 +19520,7 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
19301
19520
  }
19302
19521
  parseTomlContent(content) {
19303
19522
  try {
19304
- const parsed = parse$1(content);
19523
+ const parsed = parse$2(content);
19305
19524
  const result = TabnineCommandFrontmatterSchema.safeParse(parsed);
19306
19525
  if (!result.success) throw new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`);
19307
19526
  return {
@@ -20418,6 +20637,11 @@ var CommandsProcessor = class extends FeatureProcessor {
20418
20637
  global: this.global,
20419
20638
  relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
20420
20639
  }));
20640
+ if (await refusesWriteOutsideRoot({
20641
+ logger: this.logger,
20642
+ rootPath: this.outputRoot,
20643
+ targetPath: configPath
20644
+ })) return changedCount;
20421
20645
  const currentContent = await readFileContentOrNull(configPath);
20422
20646
  if (currentContent === null) return changedCount;
20423
20647
  const nextContent = getDisabledHermesCommandsPluginConfigContent({
@@ -20440,6 +20664,11 @@ var CommandsProcessor = class extends FeatureProcessor {
20440
20664
  if (this.toolTarget !== "goose" || !this.global) return 0;
20441
20665
  if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
20442
20666
  const configPath = join(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
20667
+ if (await refusesWriteOutsideRoot({
20668
+ logger: this.logger,
20669
+ rootPath: this.outputRoot,
20670
+ targetPath: configPath
20671
+ })) return 0;
20443
20672
  const currentContent = await readFileContentOrNull(configPath);
20444
20673
  if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
20445
20674
  const nextContent = getGooseSlashCommandsConfigContent({
@@ -27539,6 +27768,302 @@ var PiHooks = class PiHooks extends ToolHooks {
27539
27768
  }
27540
27769
  };
27541
27770
  //#endregion
27771
+ //#region src/constants/pool-paths.ts
27772
+ /**
27773
+ * Pool (Poolside's coding agent CLI) configuration-layout conventions.
27774
+ *
27775
+ * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
27776
+ * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
27777
+ * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
27778
+ * path), the project-root `AGENTS.md`, and nested per-directory
27779
+ * `AGENTS.md` files from the repository root down through the working
27780
+ * directory, deeper files taking precedence. It skips ignored directories
27781
+ * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
27782
+ *
27783
+ * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
27784
+ * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
27785
+ * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
27786
+ * the skill directories of other Agent Skills tools; rulesync writes only the
27787
+ * Pool-specific roots and leaves the shared ones to their own targets.
27788
+ *
27789
+ * @see https://docs.poolside.ai/agent-instructions
27790
+ * @see https://docs.poolside.ai/skills
27791
+ * @see https://github.com/poolsideai/pool
27792
+ */
27793
+ /** Project-scoped `.poolside/` directory at the project root. */
27794
+ const POOL_DIR = ".poolside";
27795
+ /** Global config directory for Pool, relative to the home directory. */
27796
+ const POOL_GLOBAL_DIR = join(".config", "poolside");
27797
+ /** Project skills root, relative to the project root. */
27798
+ const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
27799
+ /** Global skills root, relative to the home directory. */
27800
+ const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
27801
+ /**
27802
+ * Pool's settings file name. MCP servers live under the top-level
27803
+ * `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
27804
+ * `~/.config/poolside/settings.yaml` (global); the untracked
27805
+ * `.poolside/settings.local.yaml` overlay is left to the user.
27806
+ *
27807
+ * @see https://docs.poolside.ai/mcp-servers
27808
+ * @see https://docs.poolside.ai/settings-file-reference
27809
+ */
27810
+ const POOL_SETTINGS_FILE_NAME = "settings.yaml";
27811
+ /** Pool's untracked per-user overlay next to the settings file; never written. */
27812
+ const POOL_SETTINGS_LOCAL_FILE_NAME = "settings.local.yaml";
27813
+ /** Top-level key of Pool's settings file that holds the MCP server map. */
27814
+ const POOL_MCP_SERVERS_KEY = "mcp_servers";
27815
+ /** Top-level key of Pool's settings file that holds per-tool permission rules. */
27816
+ const POOL_TOOLS_KEY = "tools";
27817
+ /** Top-level key of Pool's settings file that holds file access rules. */
27818
+ const POOL_PATHS_KEY = "paths";
27819
+ /** Top-level key of Pool's settings file that holds the per-event hook lists. */
27820
+ const POOL_HOOKS_KEY = "hooks";
27821
+ /**
27822
+ * Top-level key of Pool's settings file that holds the subagent registry:
27823
+ * `subagents.default` names the agent delegated to by default and
27824
+ * `subagents.agents.<name>` defines each agent.
27825
+ *
27826
+ * @see https://docs.poolside.ai/subagents
27827
+ */
27828
+ const POOL_SUBAGENTS_KEY = "subagents";
27829
+ /** Key under `subagents` that holds the per-agent definitions. */
27830
+ const POOL_SUBAGENTS_AGENTS_KEY = "agents";
27831
+ /** Pool's reserved built-in agent, always `in_process` and never generated. */
27832
+ const POOL_GENERAL_AGENT_NAME = "general";
27833
+ //#endregion
27834
+ //#region src/features/hooks/pool-hooks.ts
27835
+ /** The matcher Pool reads as "any tool"; `""` means the same. */
27836
+ const POOL_MATCH_ALL = "*";
27837
+ const SUPPORTED_POOL_EVENTS = new Set(POOL_HOOK_EVENTS);
27838
+ /**
27839
+ * The Pool events that test `matcher` against a tool name. Pool ignores the
27840
+ * field on every other event (it stays required by the schema), so an
27841
+ * authored matcher there is dropped with a warning and `*` written instead.
27842
+ */
27843
+ const POOL_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
27844
+ /**
27845
+ * Single spelling of the settings.yaml codec/policy, matching PoolMcp and
27846
+ * PoolPermissions: fail closed on an unparseable root rather than replacing
27847
+ * the user's primary Pool settings with generated output.
27848
+ */
27849
+ function parsePoolSettings$3(fileContent, filePath) {
27850
+ return parseSharedConfig({
27851
+ format: "yaml",
27852
+ fileContent,
27853
+ filePath,
27854
+ invalidRootPolicy: "error"
27855
+ });
27856
+ }
27857
+ /**
27858
+ * Build the `hooks` event lists of Pool's settings file from a canonical hooks
27859
+ * config. Pool keys a flat list of `{name, matcher, command, timeout}` entries
27860
+ * by event name. Only `type: "command"` canonical hooks are emitted, since a
27861
+ * Pool hook is a shell command. A shared canonical event Pool does not fire is
27862
+ * skipped (the HooksProcessor reports it), while an event under the
27863
+ * `pool.hooks` override — such as one an import filed there — is written
27864
+ * verbatim so it round-trips.
27865
+ */
27866
+ function canonicalToPoolHooks({ config, toolOverride, logger }) {
27867
+ const sharedHooks = {};
27868
+ for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_POOL_EVENTS.has(event)) sharedHooks[event] = defs;
27869
+ const effective = {
27870
+ ...sharedHooks,
27871
+ ...toolOverride
27872
+ };
27873
+ const hooks = {};
27874
+ for (const [event, defs] of Object.entries(effective)) {
27875
+ if (isPrototypePollutionKey(event)) continue;
27876
+ const poolEvent = lookupOwn({
27877
+ record: CANONICAL_TO_POOL_EVENT_NAMES,
27878
+ key: event
27879
+ }) ?? event;
27880
+ const entries = [];
27881
+ for (const def of defs) {
27882
+ const entry = canonicalDefToPoolEntry({
27883
+ def,
27884
+ event,
27885
+ poolEvent,
27886
+ logger
27887
+ });
27888
+ if (entry !== null) entries.push(entry);
27889
+ }
27890
+ if (entries.length > 0) hooks[poolEvent] = entries;
27891
+ }
27892
+ return hooks;
27893
+ }
27894
+ /** Convert one canonical hook definition to a Pool entry, or null to skip. */
27895
+ function canonicalDefToPoolEntry({ def, event, poolEvent, logger }) {
27896
+ if ((def.type ?? "command") !== "command") return null;
27897
+ if (typeof def.command !== "string" || def.command === "") {
27898
+ logger?.warn(`Pool hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Pool would reject, so it was skipped.`);
27899
+ return null;
27900
+ }
27901
+ const authoredMatcher = typeof def.matcher === "string" && def.matcher !== "" ? def.matcher : POOL_MATCH_ALL;
27902
+ let matcher = authoredMatcher;
27903
+ if (!POOL_MATCHER_EVENTS.has(poolEvent) && authoredMatcher !== POOL_MATCH_ALL) {
27904
+ logger?.warn(`Pool ignores "matcher" on "${poolEvent}", so the matcher ${quoteValueForWarning(authoredMatcher)} of the hook ${quoteValueForWarning(def.name ?? def.command)} under "${event}" was dropped.`);
27905
+ matcher = POOL_MATCH_ALL;
27906
+ }
27907
+ const entry = {
27908
+ ...typeof def.name === "string" && def.name !== "" ? { name: def.name } : {},
27909
+ matcher,
27910
+ command: def.command
27911
+ };
27912
+ if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
27913
+ return entry;
27914
+ }
27915
+ /** Convert one raw hook entry to a canonical definition, or null to skip. */
27916
+ function poolEntryToCanonicalDef({ raw, poolEvent }) {
27917
+ if (!isRecord$1(raw) || typeof raw.command !== "string" || raw.command === "") return null;
27918
+ const def = {
27919
+ type: "command",
27920
+ command: raw.command
27921
+ };
27922
+ if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
27923
+ if (POOL_MATCHER_EVENTS.has(poolEvent) && typeof raw.matcher === "string" && raw.matcher !== "" && raw.matcher !== POOL_MATCH_ALL) def.matcher = raw.matcher;
27924
+ if (typeof raw.timeout === "number") def.timeout = raw.timeout;
27925
+ return def;
27926
+ }
27927
+ /**
27928
+ * Reverse {@link canonicalToPoolHooks}: parse the event lists of the `hooks`
27929
+ * block back into a canonical event → definition[] record. An event Pool does
27930
+ * not document is carried under its own name so `buildImportedHooksConfig`
27931
+ * files it under the `pool.hooks` override; a non-list sibling such as
27932
+ * `stop_hook_max_continuations` is not a hook and is left alone.
27933
+ */
27934
+ function poolHooksToCanonical(hooksBlock) {
27935
+ const canonical = {};
27936
+ if (!isRecord$1(hooksBlock)) return canonical;
27937
+ for (const [poolEvent, rawEntries] of Object.entries(hooksBlock)) {
27938
+ if (isPrototypePollutionKey(poolEvent) || !Array.isArray(rawEntries)) continue;
27939
+ const canonicalEvent = lookupOwn({
27940
+ record: POOL_TO_CANONICAL_EVENT_NAMES,
27941
+ key: poolEvent
27942
+ }) ?? poolEvent;
27943
+ const defs = rawEntries.map((raw) => poolEntryToCanonicalDef({
27944
+ raw,
27945
+ poolEvent
27946
+ })).filter((def) => def !== null);
27947
+ if (defs.length === 0) continue;
27948
+ canonical[canonicalEvent] = [...lookupOwn({
27949
+ record: canonical,
27950
+ key: canonicalEvent
27951
+ }) ?? [], ...defs];
27952
+ }
27953
+ return canonical;
27954
+ }
27955
+ /**
27956
+ * The keys of an existing `hooks` block that are not event lists —
27957
+ * `stop_hook_max_continuations` and anything else Pool may add beside the
27958
+ * events — which survive a regenerate untouched.
27959
+ */
27960
+ function nonEventHookKeys(hooksBlock) {
27961
+ const kept = {};
27962
+ if (!isRecord$1(hooksBlock)) return kept;
27963
+ for (const [key, value] of Object.entries(hooksBlock)) if (!isPrototypePollutionKey(key) && !Array.isArray(value)) kept[key] = value;
27964
+ return kept;
27965
+ }
27966
+ /**
27967
+ * Pool hooks.
27968
+ *
27969
+ * Pool reads hooks from the `hooks` key of its settings file —
27970
+ * `.poolside/settings.yaml` at project scope and
27971
+ * `~/.config/poolside/settings.yaml` at user scope — as
27972
+ * `hooks.<Event>: [{name?, matcher, command, timeout?}]` for `PreToolUse`,
27973
+ * `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `SessionStart` and `Stop`.
27974
+ * The event lists are rewritten as a whole; the block's non-list sibling
27975
+ * `stop_hook_max_continuations` and every other top-level key of the file
27976
+ * (`mcp_servers`, `tools`, `paths`, `pool`, ...) are preserved, and the file
27977
+ * is never deleted.
27978
+ *
27979
+ * @see https://docs.poolside.ai/hooks
27980
+ * @see https://docs.poolside.ai/settings-file-reference
27981
+ */
27982
+ var PoolHooks = class PoolHooks extends ToolHooks {
27983
+ settings;
27984
+ constructor(params) {
27985
+ super(params);
27986
+ this.settings = parsePoolSettings$3(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
27987
+ }
27988
+ getSettings() {
27989
+ return this.settings;
27990
+ }
27991
+ isDeletable() {
27992
+ return false;
27993
+ }
27994
+ static getSettablePaths({ global = false } = {}) {
27995
+ return {
27996
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
27997
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
27998
+ };
27999
+ }
28000
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
28001
+ const paths = this.getSettablePaths({ global });
28002
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
28003
+ return new PoolHooks({
28004
+ outputRoot,
28005
+ relativeDirPath: paths.relativeDirPath,
28006
+ relativeFilePath: paths.relativeFilePath,
28007
+ fileContent,
28008
+ validate,
28009
+ global
28010
+ });
28011
+ }
28012
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
28013
+ const paths = this.getSettablePaths({ global });
28014
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
28015
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
28016
+ const existing = parsePoolSettings$3(existingContent, filePath);
28017
+ const config = rulesyncHooks.getJson();
28018
+ const hooks = canonicalToPoolHooks({
28019
+ config,
28020
+ toolOverride: config.pool?.hooks,
28021
+ logger
28022
+ });
28023
+ return new PoolHooks({
28024
+ outputRoot,
28025
+ relativeDirPath: paths.relativeDirPath,
28026
+ relativeFilePath: paths.relativeFilePath,
28027
+ fileContent: applySharedConfigPatch({
28028
+ fileKey: sharedConfigFileKey(paths),
28029
+ feature: "hooks",
28030
+ existingContent,
28031
+ patch: { [POOL_HOOKS_KEY]: {
28032
+ ...nonEventHookKeys(existing[POOL_HOOKS_KEY]),
28033
+ ...hooks
28034
+ } },
28035
+ filePath,
28036
+ logger
28037
+ }),
28038
+ validate,
28039
+ global
28040
+ });
28041
+ }
28042
+ toRulesyncHooks() {
28043
+ const hooks = poolHooksToCanonical(this.settings[POOL_HOOKS_KEY]);
28044
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
28045
+ hooks,
28046
+ overrideKey: "pool"
28047
+ }), null, 2) });
28048
+ }
28049
+ validate() {
28050
+ return {
28051
+ success: true,
28052
+ error: null
28053
+ };
28054
+ }
28055
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
28056
+ return new PoolHooks({
28057
+ outputRoot,
28058
+ relativeDirPath,
28059
+ relativeFilePath,
28060
+ fileContent: "",
28061
+ validate: false,
28062
+ global
28063
+ });
28064
+ }
28065
+ };
28066
+ //#endregion
27542
28067
  //#region src/features/hooks/qwencode-hooks.ts
27543
28068
  /**
27544
28069
  * Build a single Qwen Code hook object from a canonical hook definition.
@@ -28652,14 +29177,15 @@ const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
28652
29177
  /**
28653
29178
  * Event names present in the config that the target's adapter cannot emit.
28654
29179
  *
28655
- * When the factory passes override-block keys through verbatim
28656
- * (`passthroughOverrideEvents`), those keys are excluded from the check so
28657
- * documented passthrough triggers aren't falsely reported as skipped.
29180
+ * Shared (`hooks`) keys are always checked. Override-block keys are only
29181
+ * checked when the factory declares `dropsUnknownOverrideEvents`; otherwise
29182
+ * the adapter either emits them verbatim or reports its own drops, and a
29183
+ * generic "not supported" warning would be wrong or duplicated.
28658
29184
  */
28659
29185
  function unsupportedEventNames(params) {
28660
29186
  const { factory, sharedHooks, effectiveHooks } = params;
28661
29187
  const supportedEvents = new Set(factory.supportedEvents);
28662
- const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
29188
+ const eventNames = factory.dropsUnknownOverrideEvents ? Object.keys(effectiveHooks) : Object.keys(sharedHooks);
28663
29189
  return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
28664
29190
  }
28665
29191
  /**
@@ -28752,7 +29278,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28752
29278
  supportedEvents: AMP_HOOK_EVENTS,
28753
29279
  supportedHookTypes: ["command"],
28754
29280
  supportsMatcher: true,
28755
- matcherEvents: ["preToolUse", "postToolUse"]
29281
+ matcherEvents: ["preToolUse", "postToolUse"],
29282
+ dropsUnknownOverrideEvents: true
28756
29283
  }],
28757
29284
  ["antigravity-cli", {
28758
29285
  class: AntigravityCliHooks,
@@ -28785,7 +29312,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28785
29312
  },
28786
29313
  supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
28787
29314
  supportedHookTypes: ["command"],
28788
- supportsMatcher: true
29315
+ supportsMatcher: true,
29316
+ dropsUnknownOverrideEvents: true
28789
29317
  }],
28790
29318
  ["cursor", {
28791
29319
  class: CursorHooks,
@@ -28830,7 +29358,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28830
29358
  "mcp_tool",
28831
29359
  "agent"
28832
29360
  ],
28833
- supportsMatcher: true
29361
+ supportsMatcher: true,
29362
+ dropsUnknownOverrideEvents: true
28834
29363
  }],
28835
29364
  ["codexcli", {
28836
29365
  class: CodexcliHooks,
@@ -28891,7 +29420,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28891
29420
  supportedEvents: KILO_HOOK_EVENTS,
28892
29421
  supportedHookTypes: ["command"],
28893
29422
  supportsMatcher: true,
28894
- matcherEvents: ["preToolUse", "postToolUse"]
29423
+ matcherEvents: ["preToolUse", "postToolUse"],
29424
+ dropsUnknownOverrideEvents: true
28895
29425
  }],
28896
29426
  ["opencode", {
28897
29427
  class: OpencodeHooks,
@@ -28903,7 +29433,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28903
29433
  supportedEvents: OPENCODE_HOOK_EVENTS,
28904
29434
  supportedHookTypes: ["command"],
28905
29435
  supportsMatcher: true,
28906
- matcherEvents: ["preToolUse", "postToolUse"]
29436
+ matcherEvents: ["preToolUse", "postToolUse"],
29437
+ dropsUnknownOverrideEvents: true
28907
29438
  }],
28908
29439
  ["pi", {
28909
29440
  class: PiHooks,
@@ -28914,7 +29445,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28914
29445
  },
28915
29446
  supportedEvents: PI_HOOK_EVENTS,
28916
29447
  supportedHookTypes: ["command"],
28917
- supportsMatcher: true
29448
+ supportsMatcher: true,
29449
+ dropsUnknownOverrideEvents: true
28918
29450
  }],
28919
29451
  ["factorydroid", {
28920
29452
  class: FactorydroidHooks,
@@ -28936,7 +29468,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28936
29468
  },
28937
29469
  supportedEvents: CLINE_HOOK_EVENTS,
28938
29470
  supportedHookTypes: ["command"],
28939
- supportsMatcher: false
29471
+ supportsMatcher: false,
29472
+ dropsUnknownOverrideEvents: true
28940
29473
  }],
28941
29474
  ["goose", {
28942
29475
  class: GooseHooks,
@@ -28958,8 +29491,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28958
29491
  },
28959
29492
  supportedEvents: HERMESAGENT_HOOK_EVENTS,
28960
29493
  supportedHookTypes: ["command"],
28961
- supportsMatcher: true,
28962
- passthroughOverrideEvents: true
29494
+ supportsMatcher: true
28963
29495
  }],
28964
29496
  ["kimi-code", {
28965
29497
  class: KimiCodeHooks,
@@ -28970,8 +29502,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28970
29502
  },
28971
29503
  supportedEvents: KIMI_CODE_HOOK_EVENTS,
28972
29504
  supportedHookTypes: ["command"],
28973
- supportsMatcher: true,
28974
- passthroughOverrideEvents: true
29505
+ supportsMatcher: true
28975
29506
  }],
28976
29507
  ["deepagents", {
28977
29508
  class: DeepagentsHooks,
@@ -28982,7 +29513,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28982
29513
  },
28983
29514
  supportedEvents: DEEPAGENTS_HOOK_EVENTS,
28984
29515
  supportedHookTypes: ["command"],
28985
- supportsMatcher: true
29516
+ supportsMatcher: true,
29517
+ dropsUnknownOverrideEvents: true
28986
29518
  }],
28987
29519
  ["kiro", {
28988
29520
  class: KiroHooks,
@@ -28993,8 +29525,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
28993
29525
  },
28994
29526
  supportedEvents: KIRO_HOOK_EVENTS,
28995
29527
  supportedHookTypes: ["command"],
28996
- supportsMatcher: true,
28997
- passthroughOverrideEvents: true
29528
+ supportsMatcher: true
28998
29529
  }],
28999
29530
  ["kiro-cli", {
29000
29531
  class: KiroCliHooks,
@@ -29005,8 +29536,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29005
29536
  },
29006
29537
  supportedEvents: KIRO_IDE_HOOK_EVENTS,
29007
29538
  supportedHookTypes: ["command", "prompt"],
29008
- supportsMatcher: true,
29009
- passthroughOverrideEvents: true
29539
+ supportsMatcher: true
29010
29540
  }],
29011
29541
  ["kiro-ide", {
29012
29542
  class: KiroIdeHooks,
@@ -29017,8 +29547,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29017
29547
  },
29018
29548
  supportedEvents: KIRO_IDE_HOOK_EVENTS,
29019
29549
  supportedHookTypes: ["command", "prompt"],
29020
- supportsMatcher: true,
29021
- passthroughOverrideEvents: true
29550
+ supportsMatcher: true
29022
29551
  }],
29023
29552
  ["devin", {
29024
29553
  class: DevinHooks,
@@ -29136,6 +29665,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29136
29665
  },
29137
29666
  supportedEvents: VIBE_HOOK_EVENTS,
29138
29667
  supportedHookTypes: ["command"],
29668
+ supportsMatcher: true,
29669
+ dropsUnknownOverrideEvents: true
29670
+ }],
29671
+ ["pool", {
29672
+ class: PoolHooks,
29673
+ meta: {
29674
+ supportsProject: true,
29675
+ supportsGlobal: true,
29676
+ supportsImport: true
29677
+ },
29678
+ supportedEvents: POOL_HOOK_EVENTS,
29679
+ supportedHookTypes: ["command"],
29139
29680
  supportsMatcher: true
29140
29681
  }],
29141
29682
  ["qwencode", {
@@ -29163,7 +29704,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29163
29704
  },
29164
29705
  supportedEvents: REASONIX_HOOK_EVENTS,
29165
29706
  supportedHookTypes: ["command"],
29166
- supportsMatcher: true
29707
+ supportsMatcher: true,
29708
+ dropsUnknownOverrideEvents: true
29167
29709
  }],
29168
29710
  ["grokcli", {
29169
29711
  class: GrokcliHooks,
@@ -29185,8 +29727,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
29185
29727
  },
29186
29728
  supportedEvents: CRUSH_HOOK_EVENTS,
29187
29729
  supportedHookTypes: ["command"],
29188
- supportsMatcher: true,
29189
- passthroughOverrideEvents: true
29730
+ supportsMatcher: true
29190
29731
  }],
29191
29732
  ["zcode", {
29192
29733
  class: ZcodeHooks,
@@ -35820,7 +36361,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
35820
36361
  * that is not an object at all costs nothing.
35821
36362
  */
35822
36363
  function readExistingKiloMcpEntries(fileContent) {
35823
- const mcp = parse(fileContent || "{}")?.mcp;
36364
+ const mcp = parse$1(fileContent || "{}")?.mcp;
35824
36365
  if (!isRecord$1(mcp)) return {};
35825
36366
  const entries = {};
35826
36367
  for (const [serverName, entry] of Object.entries(mcp)) {
@@ -35844,7 +36385,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
35844
36385
  json;
35845
36386
  constructor(params) {
35846
36387
  super(params);
35847
- this.json = KiloConfigSchema.parse(parse(this.fileContent || "{}"));
36388
+ this.json = KiloConfigSchema.parse(parse$1(this.fileContent || "{}"));
35848
36389
  }
35849
36390
  getJson() {
35850
36391
  return this.json;
@@ -35912,7 +36453,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
35912
36453
  outputRoot,
35913
36454
  global
35914
36455
  });
35915
- const json = parse(fileContent ?? "{\"mcp\":{}}");
36456
+ const json = parse$1(fileContent ?? "{\"mcp\":{}}");
35916
36457
  const newJson = {
35917
36458
  ...json,
35918
36459
  mcp: json.mcp ?? {}
@@ -35983,7 +36524,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
35983
36524
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
35984
36525
  }
35985
36526
  if (instructions.length === 0 && fileContent === null) return null;
35986
- const json = fileContent ? parse(fileContent) : {};
36527
+ const json = fileContent ? parse$1(fileContent) : {};
35987
36528
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
35988
36529
  const managedPrefix = `${toPosixPath(join(KILO_DIR, KILO_RULES_DIR_NAME))}/`;
35989
36530
  const preservedInstructions = existingInstructions.filter((entry) => !toPosixPath(entry).replace(/^\.\//, "").startsWith(managedPrefix));
@@ -36947,7 +37488,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
36947
37488
  json;
36948
37489
  constructor(params) {
36949
37490
  super(params);
36950
- this.json = OpencodeConfigSchema.parse(parse(this.fileContent || "{}"));
37491
+ this.json = OpencodeConfigSchema.parse(parse$1(this.fileContent || "{}"));
36951
37492
  }
36952
37493
  getJson() {
36953
37494
  return this.json;
@@ -36980,7 +37521,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
36980
37521
  fileContent = await readFileContentOrNull(jsonPath);
36981
37522
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
36982
37523
  }
36983
- const json = parse(fileContent ?? "{\"mcp\":{}}");
37524
+ const json = parse$1(fileContent ?? "{\"mcp\":{}}");
36984
37525
  const newJson = {
36985
37526
  ...json,
36986
37527
  mcp: json.mcp ?? {}
@@ -37009,7 +37550,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
37009
37550
  mcpServers: rulesyncMcp.getMcpServers(),
37010
37551
  replacement: "{env:$1}"
37011
37552
  });
37012
- const existingMcp = OpencodeConfigSchema.safeParse(parse(fileContent || "{}"));
37553
+ const existingMcp = OpencodeConfigSchema.safeParse(parse$1(fileContent || "{}"));
37013
37554
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
37014
37555
  return new OpencodeMcp({
37015
37556
  outputRoot,
@@ -37060,7 +37601,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
37060
37601
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
37061
37602
  }
37062
37603
  if (instructions.length === 0 && fileContent === null) return null;
37063
- const json = fileContent ? parse(fileContent) : {};
37604
+ const json = fileContent ? parse$1(fileContent) : {};
37064
37605
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
37065
37606
  const managedPrefixes = global ? ["memories/", `${configDirPrefix}memories/`] : [`${toPosixPath(OPENCODE_DIR)}/memories/`];
37066
37607
  const preservedInstructions = existingInstructions.filter((entry) => {
@@ -37117,49 +37658,6 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
37117
37658
  }
37118
37659
  };
37119
37660
  //#endregion
37120
- //#region src/constants/pool-paths.ts
37121
- /**
37122
- * Pool (Poolside's coding agent CLI) configuration-layout conventions.
37123
- *
37124
- * Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
37125
- * describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
37126
- * honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
37127
- * path), the project-root `AGENTS.md`, and nested per-directory
37128
- * `AGENTS.md` files from the repository root down through the working
37129
- * directory, deeper files taking precedence. It skips ignored directories
37130
- * (`.git/`, `node_modules/`, cache directories, repository ignore rules).
37131
- *
37132
- * Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
37133
- * scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
37134
- * (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
37135
- * the skill directories of other Agent Skills tools; rulesync writes only the
37136
- * Pool-specific roots and leaves the shared ones to their own targets.
37137
- *
37138
- * @see https://docs.poolside.ai/agent-instructions
37139
- * @see https://docs.poolside.ai/skills
37140
- * @see https://github.com/poolsideai/pool
37141
- */
37142
- /** Project-scoped `.poolside/` directory at the project root. */
37143
- const POOL_DIR = ".poolside";
37144
- /** Global config directory for Pool, relative to the home directory. */
37145
- const POOL_GLOBAL_DIR = join(".config", "poolside");
37146
- /** Project skills root, relative to the project root. */
37147
- const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
37148
- /** Global skills root, relative to the home directory. */
37149
- const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
37150
- /**
37151
- * Pool's settings file name. MCP servers live under the top-level
37152
- * `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
37153
- * `~/.config/poolside/settings.yaml` (global); the untracked
37154
- * `.poolside/settings.local.yaml` overlay is left to the user.
37155
- *
37156
- * @see https://docs.poolside.ai/mcp-servers
37157
- * @see https://docs.poolside.ai/settings-file-reference
37158
- */
37159
- const POOL_SETTINGS_FILE_NAME = "settings.yaml";
37160
- /** Top-level key of Pool's settings file that holds the MCP server map. */
37161
- const POOL_MCP_SERVERS_KEY = "mcp_servers";
37162
- //#endregion
37163
37661
  //#region src/features/mcp/pool-mcp.ts
37164
37662
  /**
37165
37663
  * Single spelling of the settings.yaml codec/policy, matching the
@@ -37167,7 +37665,7 @@ const POOL_MCP_SERVERS_KEY = "mcp_servers";
37167
37665
  * unparseable root rather than replacing the user's primary Pool settings with
37168
37666
  * generated output.
37169
37667
  */
37170
- function parsePoolSettings(fileContent, filePath) {
37668
+ function parsePoolSettings$2(fileContent, filePath) {
37171
37669
  return parseSharedConfig({
37172
37670
  format: "yaml",
37173
37671
  fileContent,
@@ -37399,7 +37897,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
37399
37897
  settings;
37400
37898
  constructor(params) {
37401
37899
  super(params);
37402
- this.settings = parsePoolSettings(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
37900
+ this.settings = parsePoolSettings$2(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
37403
37901
  }
37404
37902
  getSettings() {
37405
37903
  return this.settings;
@@ -41243,7 +41741,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
41243
41741
  function toAntigravityCliToolName(canonical) {
41244
41742
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
41245
41743
  }
41246
- function toCanonicalToolName$8(cliName) {
41744
+ function toCanonicalToolName$9(cliName) {
41247
41745
  return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
41248
41746
  }
41249
41747
  /**
@@ -41459,7 +41957,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
41459
41957
  const processEntries = (entries, action) => {
41460
41958
  for (const entry of entries) {
41461
41959
  const { toolName, pattern } = parsePermissionEntry$1(entry);
41462
- const canonical = toCanonicalToolName$8(toolName);
41960
+ const canonical = toCanonicalToolName$9(toolName);
41463
41961
  if (!permission[canonical]) permission[canonical] = {};
41464
41962
  permission[canonical][pattern] = action;
41465
41963
  }
@@ -41738,7 +42236,7 @@ function toLegacyAugmentToolName(augmentName) {
41738
42236
  key: augmentName
41739
42237
  }) ?? augmentName;
41740
42238
  }
41741
- function toCanonicalToolName$7(augmentName) {
42239
+ function toCanonicalToolName$8(augmentName) {
41742
42240
  const legacyName = toLegacyAugmentToolName(augmentName);
41743
42241
  return lookupOwn({
41744
42242
  record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
@@ -42191,7 +42689,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
42191
42689
  const type = entry.permission.type;
42192
42690
  if (!isBasicAugmentType(type)) continue;
42193
42691
  const legacyToolName = toLegacyAugmentToolName(entry.toolName);
42194
- const canonical = toCanonicalToolName$7(entry.toolName);
42692
+ const canonical = toCanonicalToolName$8(entry.toolName);
42195
42693
  if (forbiddenMapKeys.has(canonical)) {
42196
42694
  logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
42197
42695
  continue;
@@ -42436,7 +42934,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
42436
42934
  function toClaudeToolName(canonical) {
42437
42935
  return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
42438
42936
  }
42439
- function toCanonicalToolName$6(claudeName) {
42937
+ function toCanonicalToolName$7(claudeName) {
42440
42938
  return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
42441
42939
  }
42442
42940
  /**
@@ -43350,7 +43848,7 @@ function convertClaudeToRulesyncPermissions(params) {
43350
43848
  const processEntries = (entries, action) => {
43351
43849
  for (const entry of entries) {
43352
43850
  const { toolName, pattern } = parseClaudePermissionEntry(entry);
43353
- const canonical = toCanonicalToolName$6(toolName);
43851
+ const canonical = toCanonicalToolName$7(toolName);
43354
43852
  if (!permission[canonical]) permission[canonical] = {};
43355
43853
  permission[canonical][pattern] = action;
43356
43854
  }
@@ -44211,8 +44709,8 @@ function mapBashActionToDecision(action) {
44211
44709
  //#region src/features/permissions/commandcode-permissions.ts
44212
44710
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
44213
44711
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
44214
- const CATCH_ALL_PATTERN$6 = "*";
44215
- const MCP_CANONICAL_PREFIX$5 = "mcp__";
44712
+ const CATCH_ALL_PATTERN$7 = "*";
44713
+ const MCP_CANONICAL_PREFIX$6 = "mcp__";
44216
44714
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
44217
44715
  const CATEGORY_TO_COMMANDCODE_TOOL = {
44218
44716
  bash: "Shell",
@@ -44266,10 +44764,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
44266
44764
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
44267
44765
  */
44268
44766
  function buildCommandcodeRule(category, pattern) {
44269
- const catchAll = pattern === CATCH_ALL_PATTERN$6 || pattern === "";
44270
- if (category.startsWith(MCP_CANONICAL_PREFIX$5)) return catchAll ? category : `${category}(${pattern})`;
44271
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$5}${pattern}`;
44272
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$6 : null;
44767
+ const catchAll = pattern === CATCH_ALL_PATTERN$7 || pattern === "";
44768
+ if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
44769
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
44770
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$7 : null;
44273
44771
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
44274
44772
  if (tool === void 0) return null;
44275
44773
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -44305,7 +44803,7 @@ function splitCommandcodeRule(rule) {
44305
44803
  }
44306
44804
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
44307
44805
  function isCatchAllSpecifier(inner) {
44308
- return inner === "" || inner === CATCH_ALL_PATTERN$6;
44806
+ return inner === "" || inner === CATCH_ALL_PATTERN$7;
44309
44807
  }
44310
44808
  /**
44311
44809
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -44339,34 +44837,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
44339
44837
  if (server.length === 0) return null;
44340
44838
  const everyServer = {
44341
44839
  category: "mcp",
44342
- pattern: CATCH_ALL_PATTERN$6,
44840
+ pattern: CATCH_ALL_PATTERN$7,
44343
44841
  notInAllow: true
44344
44842
  };
44345
- if (tool.startsWith(MCP_CANONICAL_PREFIX$5)) {
44346
- if (server === CATCH_ALL_PATTERN$6) return everyServer;
44347
- if (server.includes(CATCH_ALL_PATTERN$6)) return null;
44843
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
44844
+ if (server === CATCH_ALL_PATTERN$7) return everyServer;
44845
+ if (server.includes(CATCH_ALL_PATTERN$7)) return null;
44348
44846
  return {
44349
- category: rest === "" || rest === CATCH_ALL_PATTERN$6 ? `${MCP_CANONICAL_PREFIX$5}${server}` : `${MCP_CANONICAL_PREFIX$5}${remainder}`,
44350
- pattern: CATCH_ALL_PATTERN$6,
44847
+ category: rest === "" || rest === CATCH_ALL_PATTERN$7 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
44848
+ pattern: CATCH_ALL_PATTERN$7,
44351
44849
  notInAllow: false
44352
44850
  };
44353
44851
  }
44354
- const category = `${MCP_CANONICAL_PREFIX$5}${remainder}`;
44355
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$6);
44852
+ const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
44853
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$7);
44356
44854
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
44357
44855
  category,
44358
44856
  pattern: inner,
44359
44857
  notInAllow: true
44360
44858
  } : null;
44361
- if (remainder === CATCH_ALL_PATTERN$6) return everyServer;
44362
- if (rest === CATCH_ALL_PATTERN$6 && !server.includes(CATCH_ALL_PATTERN$6)) return {
44363
- category: `${MCP_CANONICAL_PREFIX$5}${server}`,
44364
- pattern: CATCH_ALL_PATTERN$6,
44859
+ if (remainder === CATCH_ALL_PATTERN$7) return everyServer;
44860
+ if (rest === CATCH_ALL_PATTERN$7 && !server.includes(CATCH_ALL_PATTERN$7)) return {
44861
+ category: `${MCP_CANONICAL_PREFIX$6}${server}`,
44862
+ pattern: CATCH_ALL_PATTERN$7,
44365
44863
  notInAllow: true
44366
44864
  };
44367
44865
  return namesTool ? {
44368
44866
  category,
44369
- pattern: CATCH_ALL_PATTERN$6,
44867
+ pattern: CATCH_ALL_PATTERN$7,
44370
44868
  notInAllow: false
44371
44869
  } : null;
44372
44870
  }
@@ -44390,14 +44888,14 @@ function friendlyToolRule({ category, inner }) {
44390
44888
  if (category === "bash") {
44391
44889
  if (isCatchAllSpecifier(inner)) return {
44392
44890
  category,
44393
- pattern: CATCH_ALL_PATTERN$6,
44891
+ pattern: CATCH_ALL_PATTERN$7,
44394
44892
  notInAllow: false
44395
44893
  };
44396
44894
  const normalized = inner.trim().replace(/\s+/g, " ");
44397
44895
  if (normalized === "" || normalized === ":*") return null;
44398
44896
  return isCatchAllSpecifier(normalized) ? {
44399
44897
  category,
44400
- pattern: CATCH_ALL_PATTERN$6,
44898
+ pattern: CATCH_ALL_PATTERN$7,
44401
44899
  notInAllow: true
44402
44900
  } : {
44403
44901
  category,
@@ -44407,7 +44905,7 @@ function friendlyToolRule({ category, inner }) {
44407
44905
  }
44408
44906
  if (isCatchAllSpecifier(inner)) return {
44409
44907
  category,
44410
- pattern: CATCH_ALL_PATTERN$6,
44908
+ pattern: CATCH_ALL_PATTERN$7,
44411
44909
  notInAllow: false
44412
44910
  };
44413
44911
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -44426,13 +44924,13 @@ function friendlyToolRule({ category, inner }) {
44426
44924
  */
44427
44925
  function parseCommandcodeRule(rule) {
44428
44926
  const { tool, inner } = splitCommandcodeRule(rule);
44429
- if (tool === CATCH_ALL_PATTERN$6) return isCatchAllSpecifier(inner) ? {
44927
+ if (tool === CATCH_ALL_PATTERN$7) return isCatchAllSpecifier(inner) ? {
44430
44928
  category: "*",
44431
- pattern: CATCH_ALL_PATTERN$6,
44929
+ pattern: CATCH_ALL_PATTERN$7,
44432
44930
  notInAllow: true
44433
44931
  } : null;
44434
44932
  const lowered = tool.toLowerCase();
44435
- if (lowered.startsWith(MCP_CANONICAL_PREFIX$5)) return parseMcpCommandcodeRule({
44933
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$6)) return parseMcpCommandcodeRule({
44436
44934
  tool,
44437
44935
  inner
44438
44936
  });
@@ -44450,12 +44948,12 @@ function parseCommandcodeRule(rule) {
44450
44948
  */
44451
44949
  function isFoldedMcpToolNameRule({ rule, category }) {
44452
44950
  const { tool } = splitCommandcodeRule(rule);
44453
- return category.startsWith(MCP_CANONICAL_PREFIX$5) && !tool.startsWith(MCP_CANONICAL_PREFIX$5);
44951
+ return category.startsWith(MCP_CANONICAL_PREFIX$6) && !tool.startsWith(MCP_CANONICAL_PREFIX$6);
44454
44952
  }
44455
44953
  /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
44456
44954
  function scopedMcpRuleTool(rule) {
44457
44955
  const { tool, inner } = splitCommandcodeRule(rule);
44458
- return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$5) ? tool : null;
44956
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$6) ? tool : null;
44459
44957
  }
44460
44958
  /**
44461
44959
  * The string entries of a Command Code permission list. Command Code reads
@@ -44498,7 +44996,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
44498
44996
  }).filter((rule) => {
44499
44997
  const parsed = parseCommandcodeRule(rule);
44500
44998
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
44501
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$6})`);
44999
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$7})`);
44502
45000
  if (key !== "allow" && isStricterAction({
44503
45001
  action: key,
44504
45002
  existing: replacement
@@ -44555,7 +45053,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
44555
45053
  const ranked = /* @__PURE__ */ new Map();
44556
45054
  const managedCategories = /* @__PURE__ */ new Set();
44557
45055
  for (const [category, rules] of Object.entries(permission)) {
44558
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$6);
45056
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$7);
44559
45057
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
44560
45058
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
44561
45059
  for (const [pattern, action] of Object.entries(rules)) {
@@ -44624,7 +45122,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
44624
45122
  function skippedAllowImportMessage({ rule, category }) {
44625
45123
  const { tool, inner } = splitCommandcodeRule(rule);
44626
45124
  if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
44627
- if (tool.startsWith(MCP_CANONICAL_PREFIX$5) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
45125
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$6) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
44628
45126
  return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
44629
45127
  }
44630
45128
  /**
@@ -44826,7 +45324,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
44826
45324
  //#endregion
44827
45325
  //#region src/features/permissions/continue-permissions.ts
44828
45326
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
44829
- const CATCH_ALL_PATTERN$5 = "*";
45327
+ const CATCH_ALL_PATTERN$6 = "*";
44830
45328
  /**
44831
45329
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
44832
45330
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -44834,7 +45332,7 @@ const CATCH_ALL_PATTERN$5 = "*";
44834
45332
  * the bare entry instead of being skipped as a pattern-specific rule.
44835
45333
  */
44836
45334
  function normalizeCatchAllPattern$1(pattern) {
44837
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
45335
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$6 : pattern;
44838
45336
  }
44839
45337
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
44840
45338
  bash: "Bash",
@@ -44863,7 +45361,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
44863
45361
  function toContinueToolName(canonical) {
44864
45362
  return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
44865
45363
  }
44866
- function toCanonicalToolName$5(continueName) {
45364
+ function toCanonicalToolName$6(continueName) {
44867
45365
  return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
44868
45366
  }
44869
45367
  /**
@@ -44871,7 +45369,7 @@ function toCanonicalToolName$5(continueName) {
44871
45369
  * `Tool(pattern)`.
44872
45370
  */
44873
45371
  function buildContinuePermissionEntry(toolName, pattern) {
44874
- return pattern === CATCH_ALL_PATTERN$5 ? toolName : `${toolName}(${pattern})`;
45372
+ return pattern === CATCH_ALL_PATTERN$6 ? toolName : `${toolName}(${pattern})`;
44875
45373
  }
44876
45374
  /**
44877
45375
  * Split a permissions.yaml entry into its tool name and argument pattern, or
@@ -44885,7 +45383,7 @@ function parseContinuePermissionEntry(entry) {
44885
45383
  if (toolName === "") return;
44886
45384
  return {
44887
45385
  toolName,
44888
- pattern: pattern === "" ? CATCH_ALL_PATTERN$5 : pattern
45386
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$6 : pattern
44889
45387
  };
44890
45388
  }
44891
45389
  /**
@@ -45059,7 +45557,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
45059
45557
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
45060
45558
  continue;
45061
45559
  }
45062
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$5) {
45560
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$6) {
45063
45561
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
45064
45562
  continue;
45065
45563
  }
@@ -45084,7 +45582,7 @@ function convertContinuePermissionsToRulesync(lists) {
45084
45582
  const parsedEntry = parseContinuePermissionEntry(entry);
45085
45583
  if (parsedEntry === void 0) continue;
45086
45584
  if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
45087
- const category = toCanonicalToolName$5(parsedEntry.toolName);
45585
+ const category = toCanonicalToolName$6(parsedEntry.toolName);
45088
45586
  permission[category] ??= {};
45089
45587
  permission[category][parsedEntry.pattern] ??= action;
45090
45588
  }
@@ -45408,14 +45906,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
45408
45906
  };
45409
45907
  //#endregion
45410
45908
  //#region src/features/permissions/crush-permissions.ts
45411
- const CATCH_ALL_PATTERN$4 = "*";
45909
+ const CATCH_ALL_PATTERN$5 = "*";
45412
45910
  /**
45413
45911
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
45414
45912
  * argument patterns at all, so it is the only pattern a rule can carry and
45415
45913
  * still be written.
45416
45914
  */
45417
45915
  function normalizeCatchAllPattern(pattern) {
45418
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$4 : pattern;
45916
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
45419
45917
  }
45420
45918
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
45421
45919
  bash: "bash",
@@ -45427,14 +45925,14 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
45427
45925
  webfetch: "fetch"
45428
45926
  };
45429
45927
  const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
45430
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
45928
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
45431
45929
  const MCP_CRUSH_PREFIX = "mcp_";
45432
45930
  function toCrushToolName(canonical) {
45433
45931
  if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
45434
- if (canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
45932
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
45435
45933
  return canonical;
45436
45934
  }
45437
- function toCanonicalToolName$4(crushName) {
45935
+ function toCanonicalToolName$5(crushName) {
45438
45936
  return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
45439
45937
  }
45440
45938
  function isMcpToolName(crushName) {
@@ -45614,7 +46112,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
45614
46112
  for (const [rawPattern, action] of Object.entries(rules)) {
45615
46113
  if (isPrototypePollutionKey(rawPattern)) continue;
45616
46114
  const pattern = normalizeCatchAllPattern(rawPattern);
45617
- if (pattern === CATCH_ALL_PATTERN$4) {
46115
+ if (pattern === CATCH_ALL_PATTERN$5) {
45618
46116
  catchAllAction = action;
45619
46117
  continue;
45620
46118
  }
@@ -45687,11 +46185,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
45687
46185
  };
45688
46186
  for (const entry of disabled) {
45689
46187
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
45690
- bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] = "deny";
46188
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] = "deny";
45691
46189
  }
45692
46190
  for (const entry of allowed) {
45693
46191
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
45694
- bucketFor(toCanonicalToolName$4(entry))[CATCH_ALL_PATTERN$4] ??= "allow";
46192
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] ??= "allow";
45695
46193
  }
45696
46194
  return { permission };
45697
46195
  }
@@ -45738,13 +46236,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
45738
46236
  WebFetch: "webfetch",
45739
46237
  Mcp: "mcp"
45740
46238
  };
45741
- const MCP_CANONICAL_PREFIX$3 = "mcp__";
46239
+ const MCP_CANONICAL_PREFIX$4 = "mcp__";
45742
46240
  /**
45743
46241
  * Returns true if the canonical category is the per-tool MCP form
45744
46242
  * `mcp__<server>__<tool>`.
45745
46243
  */
45746
46244
  function isMcpScopedCategory(canonical) {
45747
- return canonical.startsWith(MCP_CANONICAL_PREFIX$3) && canonical.length > 5;
46245
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5;
45748
46246
  }
45749
46247
  function toCursorType(canonical) {
45750
46248
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -45778,7 +46276,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
45778
46276
  if (match) {
45779
46277
  const server = match[1] ?? "*";
45780
46278
  const tool = match[2] ?? "*";
45781
- return `${MCP_CANONICAL_PREFIX$3}${server}__${tool}`;
46279
+ return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
45782
46280
  }
45783
46281
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
45784
46282
  }
@@ -46071,7 +46569,7 @@ function convertCursorToRulesyncPermissions(params) {
46071
46569
  const { type, pattern } = parseCursorPermissionEntry(entry);
46072
46570
  const canonical = toCanonicalCategory$1(type, pattern);
46073
46571
  if (!permission[canonical]) permission[canonical] = {};
46074
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$3) ? "*" : pattern;
46572
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$4) ? "*" : pattern;
46075
46573
  permission[canonical][canonicalPattern] = action;
46076
46574
  }
46077
46575
  };
@@ -47590,7 +48088,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
47590
48088
  //#region src/features/permissions/goose-permissions.ts
47591
48089
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
47592
48090
  const GOOSE_USER_KEY = "user";
47593
- const CATCH_ALL_PATTERN$3 = "*";
48091
+ const CATCH_ALL_PATTERN$4 = "*";
47594
48092
  const RULESYNC_TO_GOOSE_TOOL_NAME = {
47595
48093
  bash: "developer__shell",
47596
48094
  edit: "developer__text_editor",
@@ -47752,7 +48250,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
47752
48250
  for (const [category, rules] of orderedEntries) {
47753
48251
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
47754
48252
  for (const [pattern, action] of Object.entries(rules)) {
47755
- if (pattern !== CATCH_ALL_PATTERN$3) {
48253
+ if (pattern !== CATCH_ALL_PATTERN$4) {
47756
48254
  logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
47757
48255
  continue;
47758
48256
  }
@@ -47781,7 +48279,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
47781
48279
  for (const toolName of toolNames) {
47782
48280
  const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
47783
48281
  permission[category] ??= {};
47784
- permission[category][CATCH_ALL_PATTERN$3] = action;
48282
+ permission[category][CATCH_ALL_PATTERN$4] = action;
47785
48283
  }
47786
48284
  }
47787
48285
  return { permission };
@@ -47792,8 +48290,8 @@ const GROKCLI_UI_KEY = "ui";
47792
48290
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
47793
48291
  const GROKCLI_PERMISSION_KEY = "permission";
47794
48292
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
47795
- const CATCH_ALL_PATTERN$2 = "*";
47796
- const MCP_CANONICAL_PREFIX$2 = "mcp__";
48293
+ const CATCH_ALL_PATTERN$3 = "*";
48294
+ const MCP_CANONICAL_PREFIX$3 = "mcp__";
47797
48295
  const CATEGORY_TO_GROK_TOOL = {
47798
48296
  bash: "Bash",
47799
48297
  read: "Read",
@@ -47834,14 +48332,14 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
47834
48332
  * concrete pattern emits `Tool(pattern)`.
47835
48333
  */
47836
48334
  function buildGrokEntry(category, pattern) {
47837
- if (category.startsWith(MCP_CANONICAL_PREFIX$2)) {
48335
+ if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
47838
48336
  const remainder = category.slice(5);
47839
48337
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
47840
48338
  }
47841
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
48339
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
47842
48340
  const tool = CATEGORY_TO_GROK_TOOL[category];
47843
48341
  if (tool === void 0) return null;
47844
- return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
48342
+ return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? tool : `${tool}(${pattern})`;
47845
48343
  }
47846
48344
  /**
47847
48345
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -47861,17 +48359,17 @@ function parseGrokEntry(entry) {
47861
48359
  inner = trimmed.slice(parenIndex + 1, -1).trim();
47862
48360
  }
47863
48361
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
47864
- category: `${MCP_CANONICAL_PREFIX$2}${inner}`,
47865
- pattern: CATCH_ALL_PATTERN$2
48362
+ category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
48363
+ pattern: CATCH_ALL_PATTERN$3
47866
48364
  } : {
47867
48365
  category: "mcp",
47868
- pattern: CATCH_ALL_PATTERN$2
48366
+ pattern: CATCH_ALL_PATTERN$3
47869
48367
  };
47870
48368
  const category = GROK_TOOL_TO_CATEGORY[tool];
47871
48369
  if (category === void 0) return null;
47872
48370
  return {
47873
48371
  category,
47874
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
48372
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$3
47875
48373
  };
47876
48374
  }
47877
48375
  /**
@@ -47892,15 +48390,15 @@ function parseGrokRule(rule) {
47892
48390
  if (typeof tool !== "string") return null;
47893
48391
  if (pattern !== void 0 && typeof pattern !== "string") return null;
47894
48392
  const trimmedPattern = pattern?.trim() ?? "";
47895
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
48393
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$3;
47896
48394
  const lowerTool = tool.trim().toLowerCase();
47897
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
48395
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$3 ? {
47898
48396
  category: "mcp",
47899
- pattern: CATCH_ALL_PATTERN$2,
48397
+ pattern: CATCH_ALL_PATTERN$3,
47900
48398
  action
47901
48399
  } : {
47902
- category: `${MCP_CANONICAL_PREFIX$2}${resolvedPattern}`,
47903
- pattern: CATCH_ALL_PATTERN$2,
48400
+ category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
48401
+ pattern: CATCH_ALL_PATTERN$3,
47904
48402
  action
47905
48403
  };
47906
48404
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -48048,7 +48546,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
48048
48546
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
48049
48547
  }
48050
48548
  const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
48051
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
48549
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$3]: legacyModeAction(parsed) } } };
48052
48550
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
48053
48551
  }
48054
48552
  validate() {
@@ -48784,7 +49282,7 @@ function isSharedKiloCategory(key) {
48784
49282
  */
48785
49283
  function parseKiloJsoncStrict(content, filePath) {
48786
49284
  const errors = [];
48787
- const parsed = parse(content, errors, { allowTrailingComma: true });
49285
+ const parsed = parse$1(content, errors, { allowTrailingComma: true });
48788
49286
  const first = errors[0];
48789
49287
  if (first) throw new Error(`Failed to parse Kilo Code config at ${filePath}: ${printParseErrorCode(first.error)} at offset ${first.offset}`);
48790
49288
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
@@ -48828,7 +49326,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
48828
49326
  json;
48829
49327
  constructor(params) {
48830
49328
  super(params);
48831
- const parsed = parse(this.fileContent || "{}");
49329
+ const parsed = parse$1(this.fileContent || "{}");
48832
49330
  if (params.validate !== false) this.json = KiloPermissionsConfigSchema.parse(parsed);
48833
49331
  else {
48834
49332
  const result = KiloPermissionsConfigSchema.safeParse(parsed);
@@ -48968,7 +49466,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
48968
49466
  }
48969
49467
  validate() {
48970
49468
  try {
48971
- const json = parse(this.fileContent || "{}");
49469
+ const json = parse$1(this.fileContent || "{}");
48972
49470
  const result = KiloPermissionsConfigSchema.safeParse(json);
48973
49471
  if (!result.success) return {
48974
49472
  success: false,
@@ -49679,7 +50177,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
49679
50177
  json;
49680
50178
  constructor(params) {
49681
50179
  super(params);
49682
- this.json = OpencodePermissionsConfigSchema.parse(parse(this.fileContent || "{}"));
50180
+ this.json = OpencodePermissionsConfigSchema.parse(parse$1(this.fileContent || "{}"));
49683
50181
  }
49684
50182
  getJson() {
49685
50183
  return this.json;
@@ -49707,7 +50205,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
49707
50205
  fileContent = await readFileContentOrNull(jsonPath);
49708
50206
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
49709
50207
  }
49710
- const parsed = parse(fileContent ?? "{}");
50208
+ const parsed = parse$1(fileContent ?? "{}");
49711
50209
  const record = isRecord$1(parsed) ? parsed : {};
49712
50210
  const nextJson = {
49713
50211
  ...record,
@@ -49928,6 +50426,567 @@ var PiPermissions = class PiPermissions extends ToolPermissions {
49928
50426
  }
49929
50427
  };
49930
50428
  //#endregion
50429
+ //#region src/features/permissions/pool-permissions.ts
50430
+ const POOL_ALLOW_KEY = "allow";
50431
+ const POOL_DENY_KEY = "deny";
50432
+ const POOL_DISABLED_KEY = "disabled";
50433
+ const POOL_PATH_KEY = "path";
50434
+ const POOL_WRITE_KEY = "write";
50435
+ const CATCH_ALL_PATTERN$2 = "*";
50436
+ const PATHS_CATCH_ALL_PATTERN = "**";
50437
+ const CANONICAL_TO_POOL_TOOL_NAMES = {
50438
+ bash: "shell",
50439
+ webfetch: "web_fetch",
50440
+ websearch: "web_search"
50441
+ };
50442
+ const POOL_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_TOOL_NAMES).map(([k, v]) => [v, k]));
50443
+ const READ_CATEGORY = "read";
50444
+ const WRITE_CATEGORIES = ["edit", "write"];
50445
+ const FILE_CATEGORIES = [READ_CATEGORY, ...WRITE_CATEGORIES];
50446
+ const MCP_CANONICAL_PREFIX$2 = "mcp__";
50447
+ /**
50448
+ * Single spelling of the settings.yaml codec/policy, matching the
50449
+ * `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
50450
+ * unparseable root rather than replacing the user's primary Pool settings with
50451
+ * generated output.
50452
+ */
50453
+ function parsePoolSettings$1(fileContent, filePath) {
50454
+ return parseSharedConfig({
50455
+ format: "yaml",
50456
+ fileContent,
50457
+ filePath,
50458
+ invalidRootPolicy: "error"
50459
+ });
50460
+ }
50461
+ function toPoolToolName(canonical) {
50462
+ return lookupOwn({
50463
+ record: CANONICAL_TO_POOL_TOOL_NAMES,
50464
+ key: canonical
50465
+ }) ?? canonical;
50466
+ }
50467
+ function toCanonicalToolName$4(poolName) {
50468
+ return lookupOwn({
50469
+ record: POOL_TO_CANONICAL_TOOL_NAMES,
50470
+ key: poolName
50471
+ }) ?? poolName;
50472
+ }
50473
+ function isMcpCategory(category) {
50474
+ return category.startsWith(MCP_CANONICAL_PREFIX$2) && category.length > 5;
50475
+ }
50476
+ function isFileCategory(category) {
50477
+ return FILE_CATEGORIES.includes(category);
50478
+ }
50479
+ /**
50480
+ * Pool's tool rules know one wildcard, `*`, which matches anything including
50481
+ * `/` — the same set a canonical `*` names — so a `**` run is collapsed to
50482
+ * keep the file in Pool's own spelling. `?`, `[...]` and `{a,b}` are literal
50483
+ * characters to Pool, so a pattern that relies on them cannot be written.
50484
+ */
50485
+ function toPoolToolPattern(pattern) {
50486
+ if (/[?[{]/.test(pattern)) return void 0;
50487
+ return pattern.replaceAll(/\*{2,}/g, "*");
50488
+ }
50489
+ /**
50490
+ * Pool's path globs know `*` and `**`, so a bare `*` run is written as `**`
50491
+ * to keep it matching anything. `?`, `[...]` and `{a,b}` are literal
50492
+ * characters to Pool here as well, so a pattern that relies on them cannot be
50493
+ * written.
50494
+ */
50495
+ function toPoolPathPattern(pattern) {
50496
+ if (/[?[{]/.test(pattern)) return void 0;
50497
+ return /^\*+$/.test(pattern) ? PATHS_CATCH_ALL_PATTERN : pattern;
50498
+ }
50499
+ function isHomeOrAbsolutePath(pattern) {
50500
+ return isAbsolute(pattern) || pattern === "~" || pattern.startsWith("~/");
50501
+ }
50502
+ function rulesOf(permission, category) {
50503
+ return lookupOwn({
50504
+ record: permission,
50505
+ key: category
50506
+ }) ?? {};
50507
+ }
50508
+ function patternsWithAction(rules, actions) {
50509
+ return Object.entries(rules).filter(([pattern, action]) => !isPrototypePollutionKey(pattern) && actions.includes(action)).map(([pattern]) => pattern);
50510
+ }
50511
+ function asRestrictions(patterns) {
50512
+ return uniq(patterns).map((pattern) => ({
50513
+ pattern,
50514
+ fromAllToolsCategory: false
50515
+ }));
50516
+ }
50517
+ function listPatterns(patterns) {
50518
+ return patterns.map((pattern) => quoteValueForWarning(pattern)).join(", ");
50519
+ }
50520
+ /**
50521
+ * Permissions adapter for Pool (Poolside's coding agent CLI).
50522
+ *
50523
+ * Pool keeps its permission rules in the same YAML settings file as its MCP
50524
+ * servers — `.poolside/settings.yaml` (project) / `~/.config/poolside/settings.yaml`
50525
+ * (global) — under two top-level keys:
50526
+ * - `tools.<name>`: `allow` and `deny` pattern lists matched against the
50527
+ * tool's argument (the command for `shell`, the URL for `web_fetch`, ...),
50528
+ * plus `disabled: true` to remove the tool. A denied pattern wins over an
50529
+ * allowed one; a call that matches neither prompts. Only `*` is a wildcard.
50530
+ * - `paths`: `allow` entries (`{ path }`, read-only unless `write: true`)
50531
+ * and `deny` entries (`{ path }`) with `*`/`**` globs; deny wins.
50532
+ *
50533
+ * Mapping (rulesync canonical -> Pool):
50534
+ * - `bash` -> `tools.shell`, `webfetch` -> `tools.web_fetch`,
50535
+ * `websearch` -> `tools.web_search`; any other non-file category passes
50536
+ * through as `tools.<category>`. `allow` -> `allow`, `deny` -> `deny`,
50537
+ * `ask` -> nothing (prompting is Pool's default) — but an `allow` that
50538
+ * overlaps an `ask` of any category landing on the same Pool tool, or a
50539
+ * deny Pool cannot spell, is withheld, since an allowed pattern would
50540
+ * silence the prompt the config asks for.
50541
+ * - `read` / `edit` / `write` -> `paths`: a `read` allow is a read-only
50542
+ * entry, an `edit` or `write` allow adds `write: true`, a `read` deny is a
50543
+ * deny entry. Pool cannot deny writes without denying reads, so an
50544
+ * `edit`/`write` deny that is not also a `read` deny only withholds the
50545
+ * write flag of the entries it overlaps (Pool prompts for those writes).
50546
+ * - `mcp__<server>__<tool>` is skipped: Pool approves MCP tools per server
50547
+ * under `mcp_servers.<server>.allow`, authored as `poolAllow` in
50548
+ * `.rulesync/mcp.json`. The all-tools `*` category is mirrored onto
50549
+ * `bash` by `honorAllToolsOnBash` and otherwise skipped.
50550
+ *
50551
+ * Project settings take project-relative paths and the user settings absolute
50552
+ * or `~` paths, so a path of the other kind is skipped with a warning at each
50553
+ * scope. The managed tools' `allow`/`deny` lists and, whenever the canonical
50554
+ * config carries a file category, the `paths` lists are rebuilt; every other
50555
+ * key (`disabled`, unmanaged tools, `mcp_servers`, `pool`, `sandbox`, ...) is
50556
+ * kept, and the file is never deleted.
50557
+ *
50558
+ * @see https://docs.poolside.ai/settings-file-reference
50559
+ */
50560
+ var PoolPermissions = class PoolPermissions extends ToolPermissions {
50561
+ settings;
50562
+ constructor(params) {
50563
+ super({
50564
+ ...params,
50565
+ fileContent: params.fileContent ?? ""
50566
+ });
50567
+ this.settings = parsePoolSettings$1(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
50568
+ }
50569
+ getSettings() {
50570
+ return this.settings;
50571
+ }
50572
+ isDeletable() {
50573
+ return false;
50574
+ }
50575
+ /**
50576
+ * settings.yaml is Pool's file: rulesync merges into it when it exists but
50577
+ * does not create one that holds nothing of its own — an absent file and an
50578
+ * absent rule both mean "prompt".
50579
+ */
50580
+ shouldSkipCreationWhenPayloadEmpty() {
50581
+ return true;
50582
+ }
50583
+ static getSettablePaths({ global = false } = {}) {
50584
+ return {
50585
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
50586
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
50587
+ };
50588
+ }
50589
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
50590
+ const paths = this.getSettablePaths({ global });
50591
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
50592
+ return new PoolPermissions({
50593
+ outputRoot,
50594
+ relativeDirPath: paths.relativeDirPath,
50595
+ relativeFilePath: paths.relativeFilePath,
50596
+ fileContent,
50597
+ validate,
50598
+ global
50599
+ });
50600
+ }
50601
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
50602
+ const paths = this.getSettablePaths({ global });
50603
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
50604
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
50605
+ const existing = parsePoolSettings$1(existingContent, filePath);
50606
+ const patch = buildPoolPermissionsPatch({
50607
+ config: rulesyncPermissions.getJson(),
50608
+ existing,
50609
+ global,
50610
+ logger
50611
+ });
50612
+ return new PoolPermissions({
50613
+ outputRoot,
50614
+ relativeDirPath: paths.relativeDirPath,
50615
+ relativeFilePath: paths.relativeFilePath,
50616
+ fileContent: applySharedConfigPatch({
50617
+ fileKey: sharedConfigFileKey(paths),
50618
+ feature: "permissions",
50619
+ existingContent,
50620
+ patch,
50621
+ filePath,
50622
+ logger
50623
+ }),
50624
+ validate: true,
50625
+ global
50626
+ });
50627
+ }
50628
+ toRulesyncPermissions() {
50629
+ const rulesyncConfig = convertPoolToRulesync({
50630
+ tools: isRecord$1(this.settings["tools"]) ? this.settings[POOL_TOOLS_KEY] : {},
50631
+ paths: isRecord$1(this.settings["paths"]) ? this.settings[POOL_PATHS_KEY] : {}
50632
+ });
50633
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
50634
+ }
50635
+ validate() {
50636
+ return {
50637
+ success: true,
50638
+ error: null
50639
+ };
50640
+ }
50641
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
50642
+ return new PoolPermissions({
50643
+ outputRoot,
50644
+ relativeDirPath,
50645
+ relativeFilePath,
50646
+ fileContent: "",
50647
+ validate: false,
50648
+ global
50649
+ });
50650
+ }
50651
+ };
50652
+ /**
50653
+ * The deep-merge patch that rewrites the managed `tools.<name>` lists and,
50654
+ * when the canonical config carries a file category, the `paths` lists. An
50655
+ * empty list retracts its key (`undefined` deletes under deep-merge), and a
50656
+ * block is only touched when there is something to write into it or it
50657
+ * already exists, so a config without rules does not grow an empty
50658
+ * `tools: {}` / `paths: {}`.
50659
+ */
50660
+ function buildPoolPermissionsPatch({ config, existing, global, logger }) {
50661
+ const permission = honorAllToolsOnBash(config.permission);
50662
+ const existingTools = isRecord$1(existing["tools"]) ? existing[POOL_TOOLS_KEY] : void 0;
50663
+ const existingPaths = isRecord$1(existing["paths"]) ? existing[POOL_PATHS_KEY] : void 0;
50664
+ const patch = {};
50665
+ const toolsPatch = {};
50666
+ for (const [toolName, lists] of convertRulesyncToPoolTools({
50667
+ permission,
50668
+ logger
50669
+ })) {
50670
+ const existingTool = existingTools === void 0 ? void 0 : lookupOwn({
50671
+ record: existingTools,
50672
+ key: toolName
50673
+ });
50674
+ if (lists.allow.length === 0 && lists.deny.length === 0 && existingTool === void 0) continue;
50675
+ toolsPatch[toolName] = {
50676
+ [POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
50677
+ [POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
50678
+ };
50679
+ }
50680
+ if (Object.keys(toolsPatch).length > 0) patch[POOL_TOOLS_KEY] = toolsPatch;
50681
+ if (FILE_CATEGORIES.some((category) => lookupOwn({
50682
+ record: permission,
50683
+ key: category
50684
+ }))) {
50685
+ const lists = convertRulesyncToPoolPaths({
50686
+ permission,
50687
+ global,
50688
+ logger
50689
+ });
50690
+ if (lists.allow.length > 0 || lists.deny.length > 0 || existingPaths !== void 0) patch[POOL_PATHS_KEY] = {
50691
+ [POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
50692
+ [POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
50693
+ };
50694
+ }
50695
+ return patch;
50696
+ }
50697
+ /**
50698
+ * Convert one non-file category into a Pool tool's `allow`/`deny` lists. A
50699
+ * deny Pool can spell needs no withholding (Pool lets it win over an allow),
50700
+ * but an `ask` has no list of its own — a call Pool does not match prompts —
50701
+ * so an allow that overlaps one is withheld and reported, and the tool keeps
50702
+ * prompting. A deny Pool cannot spell (its pattern uses a glob form other
50703
+ * than `*`) is skipped, and then withholds the allows it overlaps the same
50704
+ * way: written, such an allow would auto-approve the very command the deny
50705
+ * was meant to stop. `restrictions` carries the ask patterns and the skipped
50706
+ * deny patterns of every category that lands on the same Pool tool, because
50707
+ * Pool matches the joined lists as one.
50708
+ */
50709
+ function convertToolCategory({ category, toolName, rules, restrictions, logger }) {
50710
+ const lists = {
50711
+ allow: [],
50712
+ deny: []
50713
+ };
50714
+ const shadowingRestrictions = createShadowingRestrictionsTest(asRestrictions(restrictions));
50715
+ for (const [rawPattern, action] of Object.entries(rules)) {
50716
+ if (isPrototypePollutionKey(rawPattern) || action === "ask") continue;
50717
+ const pattern = toPoolToolPattern(rawPattern);
50718
+ if (pattern === void 0) {
50719
+ logger?.warn(`Pool tool rules treat only "*" as a wildcard, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
50720
+ continue;
50721
+ }
50722
+ if (action === "deny") {
50723
+ lists.deny.push(pattern);
50724
+ continue;
50725
+ }
50726
+ const shadowing = shadowingRestrictions(rawPattern);
50727
+ if (shadowing.length > 0) {
50728
+ logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched call prompts), so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool keeps prompting for "${toolName}".`);
50729
+ continue;
50730
+ }
50731
+ lists.allow.push(pattern);
50732
+ }
50733
+ return {
50734
+ allow: uniq(lists.allow),
50735
+ deny: uniq(lists.deny)
50736
+ };
50737
+ }
50738
+ function unrepresentableDenies(rules) {
50739
+ return patternsWithAction(rules, ["deny"]).filter((pattern) => toPoolToolPattern(pattern) === void 0);
50740
+ }
50741
+ /**
50742
+ * Group the non-file categories by the Pool tool they land on, reporting the
50743
+ * ones Pool has no place for. Two categories can resolve to the same tool
50744
+ * (`bash` and a pass-through `shell`).
50745
+ */
50746
+ function groupCategoriesByPoolTool({ permission, logger }) {
50747
+ const byTool = /* @__PURE__ */ new Map();
50748
+ for (const [category, rules] of Object.entries(permission)) {
50749
+ if (isPrototypePollutionKey(category) || isFileCategory(category)) continue;
50750
+ if (category === "*") {
50751
+ for (const [rawPattern, action] of Object.entries(rules)) {
50752
+ if (isPrototypePollutionKey(rawPattern)) continue;
50753
+ logger?.warn(`Pool has no rule that spans every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
50754
+ }
50755
+ continue;
50756
+ }
50757
+ if (isMcpCategory(category)) {
50758
+ logger?.warn(`Pool approves MCP tools per server under "mcp_servers.<server>.allow", so the rules for "${category}" were skipped; author them as "poolAllow" on the server in .rulesync/mcp.json instead.`);
50759
+ continue;
50760
+ }
50761
+ const toolName = toPoolToolName(category);
50762
+ byTool.set(toolName, [...byTool.get(toolName) ?? [], [category, rules]]);
50763
+ }
50764
+ return byTool;
50765
+ }
50766
+ /**
50767
+ * Convert the non-file categories into Pool tool lists, keyed by Pool tool
50768
+ * name. The categories that land on the same tool are converted against the
50769
+ * union of their `ask` patterns and their lists are joined, which is safe
50770
+ * because a deny wins over an allow in Pool whichever category wrote it.
50771
+ */
50772
+ function convertRulesyncToPoolTools({ permission, logger }) {
50773
+ const byTool = /* @__PURE__ */ new Map();
50774
+ for (const [toolName, categories] of groupCategoriesByPoolTool({
50775
+ permission,
50776
+ logger
50777
+ })) {
50778
+ const restrictions = categories.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...unrepresentableDenies(rules)]);
50779
+ const lists = {
50780
+ allow: [],
50781
+ deny: []
50782
+ };
50783
+ for (const [category, rules] of categories) {
50784
+ const converted = convertToolCategory({
50785
+ category,
50786
+ toolName,
50787
+ rules,
50788
+ restrictions,
50789
+ logger
50790
+ });
50791
+ lists.allow.push(...converted.allow);
50792
+ lists.deny.push(...converted.deny);
50793
+ }
50794
+ byTool.set(toolName, {
50795
+ allow: uniq(lists.allow),
50796
+ deny: uniq(lists.deny)
50797
+ });
50798
+ }
50799
+ return byTool;
50800
+ }
50801
+ /**
50802
+ * Convert the file categories into Pool's `paths` lists.
50803
+ *
50804
+ * An allow entry grants reading, and `write: true` grants writing on top, so
50805
+ * a `read` allow is withheld by an overlapping `read` ask and a write allow by
50806
+ * an overlapping `edit`/`write` ask — and by a `read` ask, since the write
50807
+ * entry would read too. A `read` deny lands in `paths.deny`, which Pool lets
50808
+ * win over any allow, and covers an `edit`/`write` deny of the same pattern,
50809
+ * so neither withholds anything; an `edit`/`write` deny without a `read` deny
50810
+ * cannot be written (Pool's deny blocks reads as well), so it is reported and
50811
+ * only withholds the write flags it overlaps. A deny Pool cannot spell is
50812
+ * skipped the same way and withholds the allows it overlaps, so it is honored
50813
+ * by prompting rather than dropped.
50814
+ */
50815
+ function convertRulesyncToPoolPaths({ permission, global, logger }) {
50816
+ const readRules = rulesOf(permission, READ_CATEGORY);
50817
+ const writeRulesByCategory = WRITE_CATEGORIES.map((category) => [category, rulesOf(permission, category)]);
50818
+ const readAsks = patternsWithAction(readRules, ["ask"]);
50819
+ const fitsScope = ({ category, action, pattern }) => {
50820
+ if (isHomeOrAbsolutePath(pattern) === global) return true;
50821
+ logger?.warn(global ? `Pool's user settings take absolute or "~" paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ${join(POOL_DIR, POOL_SETTINGS_FILE_NAME)} (without --global) instead.` : `Pool's project settings take project-relative paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ~/${join(POOL_GLOBAL_DIR, POOL_SETTINGS_FILE_NAME)} (via --global) instead.`);
50822
+ return false;
50823
+ };
50824
+ const deny = [];
50825
+ const readRestrictions = [...readAsks];
50826
+ for (const pattern of patternsWithAction(readRules, ["deny"])) {
50827
+ const path = toPoolPathPattern(pattern);
50828
+ if (path === void 0) {
50829
+ warnSkippedPathPattern({
50830
+ category: READ_CATEGORY,
50831
+ action: "deny",
50832
+ pattern,
50833
+ logger
50834
+ });
50835
+ readRestrictions.push(pattern);
50836
+ continue;
50837
+ }
50838
+ if (!fitsScope({
50839
+ category: READ_CATEGORY,
50840
+ action: "deny",
50841
+ pattern
50842
+ })) continue;
50843
+ if (!deny.some((entry) => entry[POOL_PATH_KEY] === path)) deny.push({ [POOL_PATH_KEY]: path });
50844
+ }
50845
+ const isDeniedForRead = (pattern) => {
50846
+ const path = toPoolPathPattern(pattern);
50847
+ return path !== void 0 && deny.some((entry) => entry[POOL_PATH_KEY] === path);
50848
+ };
50849
+ const writeRestrictions = writeRulesByCategory.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...patternsWithAction(rules, ["deny"]).filter((pattern) => !isDeniedForRead(pattern))]);
50850
+ reportUnwrittenWriteDenies({
50851
+ writeRulesByCategory,
50852
+ isDeniedForRead,
50853
+ logger
50854
+ });
50855
+ const shadowingReadRestrictions = createShadowingRestrictionsTest(asRestrictions(readRestrictions));
50856
+ const shadowingWriteRestrictions = createShadowingRestrictionsTest(asRestrictions([...readRestrictions, ...writeRestrictions]));
50857
+ const allowByPath = /* @__PURE__ */ new Map();
50858
+ for (const pattern of patternsWithAction(readRules, ["allow"])) {
50859
+ if (!fitsScope({
50860
+ category: READ_CATEGORY,
50861
+ action: "allow",
50862
+ pattern
50863
+ })) continue;
50864
+ const path = toPoolPathPattern(pattern);
50865
+ if (path === void 0) {
50866
+ warnSkippedPathPattern({
50867
+ category: READ_CATEGORY,
50868
+ action: "allow",
50869
+ pattern,
50870
+ logger
50871
+ });
50872
+ continue;
50873
+ }
50874
+ const shadowing = shadowingReadRestrictions(pattern);
50875
+ if (shadowing.length > 0) {
50876
+ logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched path prompts), so the "allow" rule for "read" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}.`);
50877
+ continue;
50878
+ }
50879
+ allowByPath.set(path, { [POOL_PATH_KEY]: path });
50880
+ }
50881
+ for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["allow"])) {
50882
+ if (!fitsScope({
50883
+ category,
50884
+ action: "allow",
50885
+ pattern
50886
+ })) continue;
50887
+ const path = toPoolPathPattern(pattern);
50888
+ if (path === void 0) {
50889
+ warnSkippedPathPattern({
50890
+ category,
50891
+ action: "allow",
50892
+ pattern,
50893
+ logger
50894
+ });
50895
+ continue;
50896
+ }
50897
+ const shadowing = shadowingWriteRestrictions(pattern);
50898
+ if (shadowing.length > 0) {
50899
+ logger?.warn(`A Pool path allowed with "write: true" can be read and written freely, so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool prompts for those writes.`);
50900
+ continue;
50901
+ }
50902
+ allowByPath.set(path, {
50903
+ [POOL_PATH_KEY]: path,
50904
+ [POOL_WRITE_KEY]: true
50905
+ });
50906
+ }
50907
+ return {
50908
+ allow: [...allowByPath.values()],
50909
+ deny
50910
+ };
50911
+ }
50912
+ function warnSkippedPathPattern({ category, action, pattern, logger }) {
50913
+ logger?.warn(`Pool path rules treat only "*" and "**" as wildcards, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
50914
+ }
50915
+ function reportUnwrittenWriteDenies({ writeRulesByCategory, isDeniedForRead, logger }) {
50916
+ for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["deny"])) {
50917
+ if (isDeniedForRead(pattern)) continue;
50918
+ if (toPoolPathPattern(pattern) === void 0) {
50919
+ warnSkippedPathPattern({
50920
+ category,
50921
+ action: "deny",
50922
+ pattern,
50923
+ logger
50924
+ });
50925
+ continue;
50926
+ }
50927
+ logger?.warn(`Pool cannot deny writes without denying reads, so the "deny" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was not written and Pool prompts for those writes instead; deny the pattern under "read" as well to block it entirely.`);
50928
+ }
50929
+ }
50930
+ function isUsablePattern(pattern) {
50931
+ return typeof pattern === "string" && pattern !== "" && !isPrototypePollutionKey(pattern);
50932
+ }
50933
+ function pathOfEntry(entry) {
50934
+ return isRecord$1(entry) && isUsablePattern(entry[POOL_PATH_KEY]) ? entry[POOL_PATH_KEY] : void 0;
50935
+ }
50936
+ /**
50937
+ * Convert one `tools.<name>` block into its canonical bucket. A disabled tool
50938
+ * never runs, so it reads as a catch-all deny and its own lists are ignored;
50939
+ * a denied pattern wins over an allowed one, as it does in Pool.
50940
+ */
50941
+ function importPoolTool({ toolConfig, bucket }) {
50942
+ if (toolConfig[POOL_DISABLED_KEY] === true) {
50943
+ bucket[CATCH_ALL_PATTERN$2] = "deny";
50944
+ return;
50945
+ }
50946
+ const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
50947
+ const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
50948
+ for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
50949
+ for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
50950
+ }
50951
+ /**
50952
+ * Convert Pool's `tools` and `paths` blocks back into a rulesync config.
50953
+ * `mcp_servers` approvals are the mcp feature's (`poolAllow`) and are not read.
50954
+ */
50955
+ function convertPoolToRulesync({ tools, paths }) {
50956
+ const permission = {};
50957
+ const bucketFor = (category) => {
50958
+ const own = lookupOwn({
50959
+ record: permission,
50960
+ key: category
50961
+ });
50962
+ if (own !== void 0) return own;
50963
+ const created = {};
50964
+ permission[category] = created;
50965
+ return created;
50966
+ };
50967
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
50968
+ if (isPrototypePollutionKey(toolName) || !isRecord$1(toolConfig)) continue;
50969
+ importPoolTool({
50970
+ toolConfig,
50971
+ bucket: bucketFor(toCanonicalToolName$4(toolName))
50972
+ });
50973
+ }
50974
+ const denyEntries = Array.isArray(paths[POOL_DENY_KEY]) ? paths[POOL_DENY_KEY] : [];
50975
+ const allowEntries = Array.isArray(paths[POOL_ALLOW_KEY]) ? paths[POOL_ALLOW_KEY] : [];
50976
+ for (const entry of denyEntries) {
50977
+ const path = pathOfEntry(entry);
50978
+ if (path === void 0) continue;
50979
+ for (const category of FILE_CATEGORIES) bucketFor(category)[path] = "deny";
50980
+ }
50981
+ for (const entry of allowEntries) {
50982
+ const path = pathOfEntry(entry);
50983
+ if (path === void 0) continue;
50984
+ bucketFor(READ_CATEGORY)[path] ??= "allow";
50985
+ if (isRecord$1(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
50986
+ }
50987
+ return { permission };
50988
+ }
50989
+ //#endregion
49931
50990
  //#region src/features/permissions/qwencode-permissions.ts
49932
50991
  /**
49933
50992
  * Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
@@ -56030,6 +57089,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
56030
57089
  supportsImport: true
56031
57090
  }
56032
57091
  }],
57092
+ ["pool", {
57093
+ class: PoolPermissions,
57094
+ meta: {
57095
+ supportsProject: true,
57096
+ supportsGlobal: true,
57097
+ supportsImport: true
57098
+ }
57099
+ }],
56033
57100
  ["qwencode", {
56034
57101
  class: QwencodePermissions,
56035
57102
  meta: {
@@ -56693,8 +57760,19 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
56693
57760
  const changedPaths = [];
56694
57761
  for (const aiDir of aiDirs) {
56695
57762
  const dirPath = aiDir.getDirPath();
56696
- let dirHasChanges = false;
56697
57763
  const mainFile = aiDir.getMainFile();
57764
+ const otherFiles = aiDir.getOtherFiles();
57765
+ const writtenPaths = [
57766
+ dirPath,
57767
+ ...mainFile ? [join(dirPath, mainFile.name)] : [],
57768
+ ...otherFiles.map((file) => join(dirPath, file.relativeFilePathToDirPath))
57769
+ ];
57770
+ if (await refusesAnyWriteOutsideRoot({
57771
+ logger: this.logger,
57772
+ rootPath: aiDir.getOutputRoot(),
57773
+ targetPaths: writtenPaths
57774
+ })) continue;
57775
+ let dirHasChanges = false;
56698
57776
  let mainFileContent;
56699
57777
  if (mainFile) {
56700
57778
  const mainFilePath = join(dirPath, mainFile.name);
@@ -56706,7 +57784,6 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
56706
57784
  existing: existingContent
56707
57785
  })) dirHasChanges = true;
56708
57786
  }
56709
- const otherFiles = aiDir.getOtherFiles();
56710
57787
  for (const file of otherFiles) {
56711
57788
  if (dirHasChanges) break;
56712
57789
  const filePath = join(dirPath, file.relativeFilePathToDirPath);
@@ -68825,6 +69902,325 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
68825
69902
  }
68826
69903
  };
68827
69904
  //#endregion
69905
+ //#region src/features/subagents/pool-subagent.ts
69906
+ /** How Pool runs an agent; a custom agent defaults to `in_process`. */
69907
+ const POOL_AGENT_TYPES = [
69908
+ "in_process",
69909
+ "command",
69910
+ "agent_server"
69911
+ ];
69912
+ const DEFAULT_POOL_AGENT_TYPE = "in_process";
69913
+ /**
69914
+ * One entry of `subagents.agents.<name>` in Pool's settings file. `type` is
69915
+ * required by Pool and `description` by every enabled custom agent; the
69916
+ * remaining keys depend on the type (`command`/`args`/`env` for `command`,
69917
+ * `agent_server`/`session_config_options` for `agent_server`). Loose so a key
69918
+ * Pool adds later survives the round-trip.
69919
+ * @see https://docs.poolside.ai/subagents
69920
+ * @see https://docs.poolside.ai/settings-file-reference
69921
+ */
69922
+ const PoolAgentSchema = z.looseObject({
69923
+ type: z.optional(z.enum(POOL_AGENT_TYPES)),
69924
+ description: z.optional(z.string()),
69925
+ instructions: z.optional(z.string()),
69926
+ command: z.optional(z.string()),
69927
+ args: z.optional(z.array(z.string())),
69928
+ env: z.optional(z.record(z.string(), z.string())),
69929
+ agent_server: z.optional(z.string()),
69930
+ session_config_options: z.optional(z.record(z.string(), z.unknown())),
69931
+ inherit_agent_config: z.optional(z.boolean()),
69932
+ disabled: z.optional(z.boolean())
69933
+ });
69934
+ /**
69935
+ * Single spelling of the settings.yaml codec/policy, matching PoolMcp: fail
69936
+ * closed on an unparseable root rather than replacing the user's primary Pool
69937
+ * settings with generated output.
69938
+ */
69939
+ function parsePoolSettings(fileContent, filePath) {
69940
+ return parseSharedConfig({
69941
+ format: "yaml",
69942
+ fileContent,
69943
+ filePath,
69944
+ invalidRootPolicy: "error"
69945
+ });
69946
+ }
69947
+ /**
69948
+ * The file stem an imported agent lands under in `.rulesync/subagents/`. Pool
69949
+ * keys agents by an arbitrary YAML mapping key, so the name is narrowed to a
69950
+ * safe file name before it becomes a path (defense in depth on top of the
69951
+ * central path-traversal guard); the agent keeps its real name in `name`.
69952
+ */
69953
+ function sanitizePoolAgentFileStem(raw) {
69954
+ const stem = raw.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
69955
+ return stem.length > 0 ? stem : "agent";
69956
+ }
69957
+ /**
69958
+ * Read `subagents.agents` out of a parsed settings document. The reserved
69959
+ * `general` agent is Pool's built-in and stays the user's to configure, so it
69960
+ * is never imported; an entry that is not a mapping is skipped.
69961
+ */
69962
+ function agentsOfSettings({ settings, filePath, logger }) {
69963
+ const block = settings[POOL_SUBAGENTS_KEY];
69964
+ const rawAgents = isRecord$1(block) ? block[POOL_SUBAGENTS_AGENTS_KEY] : void 0;
69965
+ const agents = {};
69966
+ if (!isRecord$1(rawAgents)) return agents;
69967
+ for (const [name, raw] of Object.entries(rawAgents)) {
69968
+ if (isPrototypePollutionKey(name) || name === "general") continue;
69969
+ const result = PoolAgentSchema.safeParse(raw);
69970
+ if (!result.success) {
69971
+ logger?.warn(`Skipped Pool subagent ${quoteValueForWarning(name)} in ${filePath}: ${formatError(result.error)}`);
69972
+ continue;
69973
+ }
69974
+ agents[name] = result.data;
69975
+ }
69976
+ return agents;
69977
+ }
69978
+ /**
69979
+ * Pool subagents.
69980
+ *
69981
+ * Pool defines subagents inside its settings file — `.poolside/settings.yaml`
69982
+ * at project scope and `~/.config/poolside/settings.yaml` at user scope —
69983
+ * under `subagents.agents.<name>`, so every targeted rulesync subagent is
69984
+ * collapsed into that one file: the body becomes `instructions`, the shared
69985
+ * `description` is written as-is and the `pool:` frontmatter section supplies
69986
+ * `type` (default `in_process`) and the type-specific keys. rulesync owns the
69987
+ * `agents` map except for Pool's built-in `general` entry, which is carried
69988
+ * over untouched together with `subagents.default` and every other top-level
69989
+ * key of the file (`mcp_servers`, `tools`, `pool`, ...); the file is never
69990
+ * deleted.
69991
+ *
69992
+ * @see https://docs.poolside.ai/subagents
69993
+ * @see https://docs.poolside.ai/settings-file-reference
69994
+ */
69995
+ var PoolSubagent = class extends ToolSubagent {
69996
+ agents;
69997
+ logger;
69998
+ /**
69999
+ * The settings document to write, built lazily: the writer hands over the
70000
+ * existing file through `setFileContent` first, and a file that does not
70001
+ * exist yet gets the block merged into an empty document. Lazy so that a
70002
+ * `forDeletion` instance never consults the gateway.
70003
+ */
70004
+ mergedContent = null;
70005
+ constructor({ agents, logger, ...rest }) {
70006
+ super({
70007
+ ...rest,
70008
+ fileContent: ""
70009
+ });
70010
+ this.agents = agents;
70011
+ this.logger = logger;
70012
+ }
70013
+ getAgents() {
70014
+ return this.agents;
70015
+ }
70016
+ isDeletable() {
70017
+ return false;
70018
+ }
70019
+ shouldMergeExistingFileContent() {
70020
+ return true;
70021
+ }
70022
+ setFileContent(newFileContent) {
70023
+ this.mergedContent = this.mergeInto(newFileContent);
70024
+ }
70025
+ getFileContent() {
70026
+ if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
70027
+ return this.mergedContent;
70028
+ }
70029
+ /**
70030
+ * Rebuild the `subagents` block on top of an existing settings document:
70031
+ * `default` and any other sibling of `agents` are kept, the built-in
70032
+ * `general` agent is carried over, and every other agent is replaced by the
70033
+ * generated set — an agent removed from `.rulesync/subagents/` must not
70034
+ * survive here. A block left empty is retracted rather than written as `{}`.
70035
+ */
70036
+ mergeInto(existingContent) {
70037
+ const paths = {
70038
+ relativeDirPath: this.getRelativeDirPath(),
70039
+ relativeFilePath: this.getRelativeFilePath()
70040
+ };
70041
+ const filePath = join(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
70042
+ const existing = parsePoolSettings(existingContent, filePath);
70043
+ const existingBlock = isRecord$1(existing["subagents"]) ? existing[POOL_SUBAGENTS_KEY] : {};
70044
+ const block = {};
70045
+ for (const [key, value] of Object.entries(existingBlock)) if (!isPrototypePollutionKey(key) && key !== "agents") block[key] = value;
70046
+ const existingAgents = isRecord$1(existingBlock["agents"]) ? existingBlock[POOL_SUBAGENTS_AGENTS_KEY] : {};
70047
+ const general = existingAgents[POOL_GENERAL_AGENT_NAME];
70048
+ const agents = {
70049
+ ...general !== void 0 ? { [POOL_GENERAL_AGENT_NAME]: general } : {},
70050
+ ...this.agents
70051
+ };
70052
+ const dropped = Object.keys(existingAgents).filter((name) => name !== "general" && !Object.hasOwn(agents, name));
70053
+ if (dropped.length > 0) {
70054
+ const advice = this.global ? "hand-written agents belong in .rulesync/subagents/" : `hand-written agents belong in ${POOL_SETTINGS_LOCAL_FILE_NAME}`;
70055
+ this.logger?.warn(`Removed Pool subagent(s) ${dropped.map(quoteValueForWarning).join(", ")} from ${filePath}: rulesync recomputes "${POOL_SUBAGENTS_KEY}.${POOL_SUBAGENTS_AGENTS_KEY}" from its own subagents on every run, so ${advice}.`);
70056
+ }
70057
+ if (Object.keys(agents).length > 0) block[POOL_SUBAGENTS_AGENTS_KEY] = agents;
70058
+ return applySharedConfigPatch({
70059
+ fileKey: sharedConfigFileKey(paths),
70060
+ feature: "subagents",
70061
+ existingContent,
70062
+ patch: { [POOL_SUBAGENTS_KEY]: Object.keys(block).length > 0 ? block : void 0 },
70063
+ filePath,
70064
+ logger: this.logger
70065
+ });
70066
+ }
70067
+ /**
70068
+ * Every agent lives in the one settings file, which the MCP feature writes
70069
+ * too: naming the file here keeps the gitignore derivation from claiming the
70070
+ * whole directory and lets the shared-file derivation order the writers.
70071
+ */
70072
+ static getSettablePaths({ global = false } = {}) {
70073
+ return {
70074
+ relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
70075
+ relativeFilePath: POOL_SETTINGS_FILE_NAME
70076
+ };
70077
+ }
70078
+ /**
70079
+ * Map a single rulesync subagent to a Pool agent entry. The body becomes
70080
+ * `instructions` and the shared `description` is written as-is; the
70081
+ * optional `pool:` section supplies `type` and the type-specific keys and
70082
+ * may override both. Returns `null` for the reserved `general` name, which
70083
+ * Pool defines itself.
70084
+ */
70085
+ static toPoolAgent({ rulesyncSubagent, logger }) {
70086
+ const frontmatter = rulesyncSubagent.getFrontmatter();
70087
+ const name = frontmatter.name;
70088
+ if (name === "general") {
70089
+ logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: "${POOL_GENERAL_AGENT_NAME}" is Pool's built-in agent, so configure it in settings.yaml directly.`);
70090
+ return null;
70091
+ }
70092
+ if (isPrototypePollutionKey(name)) {
70093
+ logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: unsafe name.`);
70094
+ return null;
70095
+ }
70096
+ const rawSection = isRecord$1(frontmatter.pool) ? frontmatter.pool : {};
70097
+ const result = PoolAgentSchema.safeParse(rawSection);
70098
+ if (!result.success) throw new Error(`Invalid "pool" section of subagent ${quoteValueForWarning(name)}: ${formatError(result.error)}`);
70099
+ const { type, description, instructions, ...rest } = result.data;
70100
+ const body = rulesyncSubagent.getBody().trim();
70101
+ const effectiveDescription = description ?? frontmatter.description;
70102
+ const effectiveInstructions = instructions ?? (body !== "" ? body : void 0);
70103
+ if (effectiveDescription === void 0 && rest.disabled !== true) logger?.warn(`Pool subagent ${quoteValueForWarning(name)} has no description; Pool requires one for every enabled custom agent.`);
70104
+ return {
70105
+ name,
70106
+ agent: {
70107
+ type: type ?? DEFAULT_POOL_AGENT_TYPE,
70108
+ ...effectiveDescription !== void 0 ? { description: effectiveDescription } : {},
70109
+ ...effectiveInstructions !== void 0 ? { instructions: effectiveInstructions } : {},
70110
+ ...rest
70111
+ }
70112
+ };
70113
+ }
70114
+ /**
70115
+ * Aggregate every targeted rulesync subagent into the one settings file.
70116
+ * Agents are keyed by their `name`; a repeated name keeps the last one, with
70117
+ * a warning, so the map stays what Pool would read.
70118
+ */
70119
+ static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true, global = false, logger }) {
70120
+ const agents = {};
70121
+ for (const rulesyncSubagent of rulesyncSubagents) {
70122
+ const converted = this.toPoolAgent({
70123
+ rulesyncSubagent,
70124
+ logger
70125
+ });
70126
+ if (converted === null) continue;
70127
+ if (Object.hasOwn(agents, converted.name)) logger?.warn(`Pool subagent ${quoteValueForWarning(converted.name)} is defined more than once; the last definition (${rulesyncSubagent.getRelativePathFromCwd()}) wins.`);
70128
+ agents[converted.name] = converted.agent;
70129
+ }
70130
+ return new this({
70131
+ outputRoot,
70132
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
70133
+ relativeFilePath: POOL_SETTINGS_FILE_NAME,
70134
+ agents,
70135
+ validate,
70136
+ global,
70137
+ logger
70138
+ });
70139
+ }
70140
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
70141
+ return this.fromRulesyncSubagents({
70142
+ outputRoot,
70143
+ rulesyncSubagents: [rulesyncSubagent],
70144
+ validate,
70145
+ global,
70146
+ logger
70147
+ });
70148
+ }
70149
+ /**
70150
+ * Convert every custom agent of the settings file back into an individual
70151
+ * rulesync subagent. `instructions` becomes the body, `description` the
70152
+ * shared field, and everything else — `type` only when it is not the
70153
+ * default — rides the `pool:` section so it survives the round-trip.
70154
+ */
70155
+ toRulesyncSubagents() {
70156
+ return Object.entries(this.agents).map(([name, agent]) => {
70157
+ const { type, description, instructions, ...rest } = agent;
70158
+ const poolSection = {
70159
+ ...type !== void 0 && type !== DEFAULT_POOL_AGENT_TYPE ? { type } : {},
70160
+ ...rest
70161
+ };
70162
+ const rulesyncFrontmatter = {
70163
+ targets: ["pool"],
70164
+ name,
70165
+ ...description !== void 0 ? { description } : {},
70166
+ ...Object.keys(poolSection).length > 0 ? { pool: poolSection } : {}
70167
+ };
70168
+ return new RulesyncSubagent({
70169
+ outputRoot: process.cwd(),
70170
+ frontmatter: rulesyncFrontmatter,
70171
+ body: instructions ?? "",
70172
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
70173
+ relativeFilePath: `${sanitizePoolAgentFileStem(name)}.md`,
70174
+ validate: true
70175
+ });
70176
+ });
70177
+ }
70178
+ toRulesyncSubagent() {
70179
+ const first = this.toRulesyncSubagents()[0];
70180
+ if (!first) throw new Error(`No custom subagents found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())} to convert.`);
70181
+ return first;
70182
+ }
70183
+ validate() {
70184
+ return {
70185
+ success: true,
70186
+ error: null
70187
+ };
70188
+ }
70189
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
70190
+ return this.isTargetedByRulesyncSubagentDefault({
70191
+ rulesyncSubagent,
70192
+ toolTarget: "pool"
70193
+ });
70194
+ }
70195
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, logger }) {
70196
+ const paths = this.getSettablePaths({ global });
70197
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
70198
+ const settings = parsePoolSettings(await readFileContentOrNull(filePath) ?? "", filePath);
70199
+ return new this({
70200
+ outputRoot,
70201
+ relativeDirPath: paths.relativeDirPath,
70202
+ relativeFilePath,
70203
+ agents: agentsOfSettings({
70204
+ settings,
70205
+ filePath,
70206
+ logger
70207
+ }),
70208
+ validate,
70209
+ global
70210
+ });
70211
+ }
70212
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
70213
+ return new this({
70214
+ outputRoot,
70215
+ relativeDirPath,
70216
+ relativeFilePath,
70217
+ agents: {},
70218
+ validate: false,
70219
+ global
70220
+ });
70221
+ }
70222
+ };
70223
+ //#endregion
68828
70224
  //#region src/features/subagents/reasonix-subagent.ts
68829
70225
  const ReasonixSubagentFrontmatterSchema = z.looseObject({
68830
70226
  name: z.string(),
@@ -70145,6 +71541,16 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
70145
71541
  filePattern: "*.md"
70146
71542
  }
70147
71543
  }],
71544
+ ["pool", {
71545
+ class: PoolSubagent,
71546
+ meta: {
71547
+ supportsProject: true,
71548
+ supportsSimulated: false,
71549
+ supportsGlobal: true,
71550
+ emitsEmptyAggregate: true,
71551
+ filePattern: POOL_SETTINGS_FILE_NAME
71552
+ }
71553
+ }],
70148
71554
  ["qwencode", {
70149
71555
  class: QwencodeSubagent,
70150
71556
  meta: {
@@ -70257,6 +71663,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
70257
71663
  toolTarget;
70258
71664
  global;
70259
71665
  getFactory;
71666
+ /**
71667
+ * Whether the last `loadRulesyncFiles` found a `subagents/` directory under
71668
+ * any input root. A project without one has not adopted the feature, so the
71669
+ * agents in its Pool settings are its own and must not be retracted.
71670
+ */
71671
+ rulesyncSourceDirFound = false;
70260
71672
  constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
70261
71673
  super({
70262
71674
  outputRoot,
@@ -70270,16 +71682,20 @@ var SubagentsProcessor = class extends FeatureProcessor {
70270
71682
  this.global = global;
70271
71683
  this.getFactory = getFactory;
70272
71684
  }
71685
+ emitsToolFilesForEmptySource() {
71686
+ return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
71687
+ }
70273
71688
  async convertRulesyncFilesToToolFiles(rulesyncFiles) {
70274
71689
  const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
70275
71690
  const factory = this.getFactory(this.toolTarget);
70276
71691
  const targeted = rulesyncSubagents.filter((rulesyncSubagent) => factory.class.isTargetedByRulesyncSubagent(rulesyncSubagent));
70277
71692
  if (factory.class.fromRulesyncSubagents) {
70278
- if (targeted.length === 0) return [];
71693
+ if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
70279
71694
  const toolSubagents = factory.class.fromRulesyncSubagents({
70280
71695
  outputRoot: this.outputRoot,
70281
71696
  rulesyncSubagents: targeted,
70282
- global: this.global
71697
+ global: this.global,
71698
+ logger: this.logger
70283
71699
  });
70284
71700
  return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
70285
71701
  }
@@ -70335,6 +71751,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
70335
71751
  this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
70336
71752
  return [];
70337
71753
  }
71754
+ this.rulesyncSourceDirFound = true;
70338
71755
  const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
70339
71756
  if (mdFiles.length === 0) {
70340
71757
  this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
@@ -70375,6 +71792,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
70375
71792
  * earlier root's copy.
70376
71793
  */
70377
71794
  async loadRulesyncFiles() {
71795
+ this.rulesyncSourceDirFound = false;
70378
71796
  const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
70379
71797
  perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
70380
71798
  identity: (subagent) => subagent.getRelativeFilePath(),
@@ -70437,7 +71855,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
70437
71855
  outputRoot: rootOutputRoot,
70438
71856
  relativeDirPath: dirPath,
70439
71857
  relativeFilePath: toRelativeFilePath(path),
70440
- global: this.global
71858
+ global: this.global,
71859
+ logger: this.logger
70441
71860
  })));
70442
71861
  toolSubagents.push(...this.claimStandaloneSubagents({
70443
71862
  loaded,
@@ -74359,22 +75778,30 @@ var DevinRule = class DevinRule extends ToolRule {
74359
75778
  }
74360
75779
  };
74361
75780
  //#endregion
74362
- //#region src/features/rules/dsh-rule.ts
75781
+ //#region src/features/rules/nested-agentsmd-rule.ts
74363
75782
  /**
74364
- * DeepSeek Harness (`dsh`) rules.
75783
+ * Shared adapter for the targets that read instructions exactly the way the
75784
+ * AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
75785
+ * project root, nested per-directory `AGENTS.md` files loaded only while working
75786
+ * under that directory, and a single personal root file under a tool-specific
75787
+ * home directory. Such a target has no modular non-root instruction directory,
75788
+ * so every other non-root rule folds into the root file.
74365
75789
  *
74366
- * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
74367
- * files — the project chain `dsh-agent-instructions` loads from the `.git`
74368
- * root down to the session cwd. Global scope writes the user-global
74369
- * `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
74370
- * single root file with no local overlay, so non-root rules fold into it.
75790
+ * Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
75791
+ * directory and their tool target through {@link getFamily}; the vendor docs
75792
+ * describing each one's discovery walk live on the subclass.
74371
75793
  *
74372
- * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
75794
+ * The nested-file scan mirrors the AGENTS.md standard's nested discovery —
75795
+ * same file name, same exclusions, import-only, project scope — because it
75796
+ * discovers literally the same files.
74373
75797
  */
74374
- var DshRule = class DshRule extends ToolRule {
75798
+ var NestedAgentsmdRule = class extends ToolRule {
75799
+ static getFamily() {
75800
+ throw new Error("Please implement this method in the subclass.");
75801
+ }
74375
75802
  static getSettablePaths({ global = false } = {}) {
74376
75803
  if (global) return { root: {
74377
- relativeDirPath: DSH_DIR,
75804
+ relativeDirPath: this.getFamily().globalDir,
74378
75805
  relativeFilePath: AGENTSMD_RULE_FILE_NAME
74379
75806
  } };
74380
75807
  return { root: {
@@ -74382,18 +75809,6 @@ var DshRule = class DshRule extends ToolRule {
74382
75809
  relativeFilePath: AGENTSMD_RULE_FILE_NAME
74383
75810
  } };
74384
75811
  }
74385
- /**
74386
- * DeepSeek Harness loads the project chain of `AGENTS.md` files from the
74387
- * project root (the nearest ancestor holding `.git`) down through the session
74388
- * cwd, broad to specific, so a per-directory file applies only while working
74389
- * under that directory. Nested files are therefore a real scoping surface,
74390
- * not just the root file's overflow.
74391
- *
74392
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
74393
- * name, same exclusions, import-only, project scope — because it discovers
74394
- * literally the same files.
74395
- * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
74396
- */
74397
75812
  static getNestedFilePatterns() {
74398
75813
  return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
74399
75814
  }
@@ -74408,7 +75823,7 @@ var DshRule = class DshRule extends ToolRule {
74408
75823
  const { root } = this.getSettablePaths({ global });
74409
75824
  if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
74410
75825
  const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
74411
- return new DshRule({
75826
+ return new this({
74412
75827
  outputRoot,
74413
75828
  relativeDirPath: overrideDirPath,
74414
75829
  relativeFilePath: AGENTSMD_RULE_FILE_NAME,
@@ -74418,7 +75833,7 @@ var DshRule = class DshRule extends ToolRule {
74418
75833
  });
74419
75834
  }
74420
75835
  const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
74421
- return new DshRule({
75836
+ return new this({
74422
75837
  outputRoot,
74423
75838
  relativeDirPath: root.relativeDirPath,
74424
75839
  relativeFilePath: root.relativeFilePath,
@@ -74432,7 +75847,7 @@ var DshRule = class DshRule extends ToolRule {
74432
75847
  const frontmatter = rulesyncRule.getFrontmatter();
74433
75848
  const isRoot = frontmatter.root ?? false;
74434
75849
  const subprojectPath = frontmatter.agentsmd?.subprojectPath;
74435
- if (!global && !isRoot && subprojectPath) return new DshRule({
75850
+ if (!global && !isRoot && subprojectPath) return new this({
74436
75851
  outputRoot,
74437
75852
  relativeDirPath: join(subprojectPath),
74438
75853
  relativeFilePath: AGENTSMD_RULE_FILE_NAME,
@@ -74440,7 +75855,7 @@ var DshRule = class DshRule extends ToolRule {
74440
75855
  validate,
74441
75856
  root: false
74442
75857
  });
74443
- return new DshRule({
75858
+ return new this({
74444
75859
  outputRoot,
74445
75860
  relativeDirPath: root.relativeDirPath,
74446
75861
  relativeFilePath: root.relativeFilePath,
@@ -74461,23 +75876,47 @@ var DshRule = class DshRule extends ToolRule {
74461
75876
  };
74462
75877
  }
74463
75878
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
74464
- return new DshRule({
75879
+ const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
75880
+ return new this({
74465
75881
  outputRoot,
74466
75882
  relativeDirPath,
74467
75883
  relativeFilePath,
74468
75884
  fileContent: "",
74469
75885
  validate: false,
74470
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".dsh")
75886
+ root: isRoot
74471
75887
  });
74472
75888
  }
74473
75889
  static isTargetedByRulesyncRule(rulesyncRule) {
74474
75890
  return this.isTargetedByRulesyncRuleDefault({
74475
75891
  rulesyncRule,
74476
- toolTarget: "dsh"
75892
+ toolTarget: this.getFamily().toolTarget
74477
75893
  });
74478
75894
  }
74479
75895
  };
74480
75896
  //#endregion
75897
+ //#region src/features/rules/dsh-rule.ts
75898
+ /**
75899
+ * DeepSeek Harness (`dsh`) rules.
75900
+ *
75901
+ * Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
75902
+ * files — the project chain `dsh-agent-instructions` loads from the `.git`
75903
+ * root down to the session cwd, broad to specific, so a per-directory file
75904
+ * applies only while working under that directory. Global scope writes the
75905
+ * user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
75906
+ * reads as a single root file with no `.local.md` overlay, so non-root rules
75907
+ * fold into it.
75908
+ *
75909
+ * @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
75910
+ */
75911
+ var DshRule = class extends NestedAgentsmdRule {
75912
+ static getFamily() {
75913
+ return {
75914
+ globalDir: DSH_DIR,
75915
+ toolTarget: "dsh"
75916
+ };
75917
+ }
75918
+ };
75919
+ //#endregion
74481
75920
  //#region src/features/rules/factorydroid-rule.ts
74482
75921
  /**
74483
75922
  * Rule generator for Factory Droid.
@@ -75871,110 +77310,24 @@ var PiRule = class PiRule extends ToolRule {
75871
77310
  };
75872
77311
  //#endregion
75873
77312
  //#region src/features/rules/pool-rule.ts
75874
- var PoolRule = class PoolRule extends ToolRule {
75875
- static getSettablePaths({ global = false } = {}) {
75876
- if (global) return { root: {
75877
- relativeDirPath: POOL_GLOBAL_DIR,
75878
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
75879
- } };
75880
- return { root: {
75881
- relativeDirPath: ".",
75882
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
75883
- } };
75884
- }
75885
- /**
75886
- * Pool reads personal, project, and directory-level `AGENTS.md` files: inside
75887
- * a git repository it loads every `AGENTS.md` from the repository root down
75888
- * through the working directory (deeper files take precedence), skipping
75889
- * ignored directories. Nested files are therefore a real scoping surface, not
75890
- * just the root file's overflow.
75891
- *
75892
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
75893
- * name, same exclusions, import-only, project scope — because it discovers
75894
- * literally the same files.
75895
- * @see https://docs.poolside.ai/agent-instructions
75896
- */
75897
- static getNestedFilePatterns() {
75898
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
75899
- }
75900
- /**
75901
- * The subproject directory this rule scopes, or `undefined` for the root file
75902
- * (project or global).
75903
- */
75904
- getSubprojectPath() {
75905
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
75906
- }
75907
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
75908
- const { root } = this.getSettablePaths({ global });
75909
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
75910
- const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
75911
- return new PoolRule({
75912
- outputRoot,
75913
- relativeDirPath: overrideDirPath,
75914
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75915
- fileContent,
75916
- validate,
75917
- root: false
75918
- });
75919
- }
75920
- const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
75921
- return new PoolRule({
75922
- outputRoot,
75923
- relativeDirPath: root.relativeDirPath,
75924
- relativeFilePath: root.relativeFilePath,
75925
- fileContent,
75926
- validate,
75927
- root: true
75928
- });
75929
- }
75930
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
75931
- const { root } = this.getSettablePaths({ global });
75932
- const frontmatter = rulesyncRule.getFrontmatter();
75933
- const isRoot = frontmatter.root ?? false;
75934
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
75935
- if (!global && !isRoot && subprojectPath) return new PoolRule({
75936
- outputRoot,
75937
- relativeDirPath: join(subprojectPath),
75938
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
75939
- fileContent: rulesyncRule.getBody(),
75940
- validate,
75941
- root: false
75942
- });
75943
- return new PoolRule({
75944
- outputRoot,
75945
- relativeDirPath: root.relativeDirPath,
75946
- relativeFilePath: root.relativeFilePath,
75947
- fileContent: rulesyncRule.getBody(),
75948
- validate,
75949
- root: isRoot
75950
- });
75951
- }
75952
- toRulesyncRule() {
75953
- const subprojectPath = this.getSubprojectPath();
75954
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
75955
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
75956
- }
75957
- validate() {
77313
+ /**
77314
+ * Pool (Poolside) rules.
77315
+ *
77316
+ * Pool reads personal, project, and directory-level `AGENTS.md` files: inside
77317
+ * a git repository it loads every `AGENTS.md` from the repository root down
77318
+ * through the working directory (deeper files take precedence), skipping
77319
+ * ignored directories. Nested files are therefore a real scoping surface, not
77320
+ * just the root file's overflow. The personal file sits below both at
77321
+ * `~/.config/poolside/AGENTS.md`.
77322
+ *
77323
+ * @see https://docs.poolside.ai/agent-instructions
77324
+ */
77325
+ var PoolRule = class extends NestedAgentsmdRule {
77326
+ static getFamily() {
75958
77327
  return {
75959
- success: true,
75960
- error: null
75961
- };
75962
- }
75963
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
75964
- return new PoolRule({
75965
- outputRoot,
75966
- relativeDirPath,
75967
- relativeFilePath,
75968
- fileContent: "",
75969
- validate: false,
75970
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === POOL_GLOBAL_DIR)
75971
- });
75972
- }
75973
- static isTargetedByRulesyncRule(rulesyncRule) {
75974
- return this.isTargetedByRulesyncRuleDefault({
75975
- rulesyncRule,
77328
+ globalDir: POOL_GLOBAL_DIR,
75976
77329
  toolTarget: "pool"
75977
- });
77330
+ };
75978
77331
  }
75979
77332
  };
75980
77333
  //#endregion
@@ -76938,110 +78291,23 @@ var TaktRule = class TaktRule extends ToolRule {
76938
78291
  //#endregion
76939
78292
  //#region src/features/rules/vibe-rule.ts
76940
78293
  const VIBE_GLOBAL_DIR = ".vibe";
76941
- var VibeRule = class VibeRule extends ToolRule {
76942
- static getSettablePaths({ global = false } = {}) {
76943
- if (global) return { root: {
76944
- relativeDirPath: VIBE_GLOBAL_DIR,
76945
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76946
- } };
76947
- return { root: {
76948
- relativeDirPath: ".",
76949
- relativeFilePath: AGENTSMD_RULE_FILE_NAME
76950
- } };
76951
- }
76952
- /**
76953
- * Vibe's harness manager walks the directories between the workspace root and
76954
- * the file being read and loads every `AGENTS.md` it finds along the way
76955
- * (`find_subdirectory_agents_md`), injecting the result into the `read_file`
76956
- * tool's output. Nested files are therefore a real scoping surface, not just
76957
- * the root file's overflow.
76958
- *
76959
- * The scan mirrors the AGENTS.md standard's nested discovery — same file
76960
- * name, same exclusions, import-only, project scope — because it discovers
76961
- * literally the same files.
76962
- * @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
76963
- */
76964
- static getNestedFilePatterns() {
76965
- return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
76966
- }
76967
- /**
76968
- * The subproject directory this rule scopes, or `undefined` for the root file
76969
- * (project or global).
76970
- */
76971
- getSubprojectPath() {
76972
- return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
76973
- }
76974
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
76975
- const { root } = this.getSettablePaths({ global });
76976
- if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
76977
- const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
76978
- return new VibeRule({
76979
- outputRoot,
76980
- relativeDirPath: overrideDirPath,
76981
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
76982
- fileContent,
76983
- validate,
76984
- root: false
76985
- });
76986
- }
76987
- const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
76988
- return new VibeRule({
76989
- outputRoot,
76990
- relativeDirPath: root.relativeDirPath,
76991
- relativeFilePath: root.relativeFilePath,
76992
- fileContent,
76993
- validate,
76994
- root: true
76995
- });
76996
- }
76997
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
76998
- const { root } = this.getSettablePaths({ global });
76999
- const frontmatter = rulesyncRule.getFrontmatter();
77000
- const isRoot = frontmatter.root ?? false;
77001
- const subprojectPath = frontmatter.agentsmd?.subprojectPath;
77002
- if (!global && !isRoot && subprojectPath) return new VibeRule({
77003
- outputRoot,
77004
- relativeDirPath: join(subprojectPath),
77005
- relativeFilePath: AGENTSMD_RULE_FILE_NAME,
77006
- fileContent: rulesyncRule.getBody(),
77007
- validate,
77008
- root: false
77009
- });
77010
- return new VibeRule({
77011
- outputRoot,
77012
- relativeDirPath: root.relativeDirPath,
77013
- relativeFilePath: root.relativeFilePath,
77014
- fileContent: rulesyncRule.getBody(),
77015
- validate,
77016
- root: isRoot
77017
- });
77018
- }
77019
- toRulesyncRule() {
77020
- const subprojectPath = this.getSubprojectPath();
77021
- if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
77022
- return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
77023
- }
77024
- validate() {
78294
+ /**
78295
+ * Mistral Vibe rules.
78296
+ *
78297
+ * Vibe's harness manager walks the directories between the workspace root and
78298
+ * the file being read and loads every `AGENTS.md` it finds along the way
78299
+ * (`find_subdirectory_agents_md`), injecting the result into the `read_file`
78300
+ * tool's output. Nested files are therefore a real scoping surface, not just
78301
+ * the root file's overflow. The personal file is `~/.vibe/AGENTS.md`.
78302
+ *
78303
+ * @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
78304
+ */
78305
+ var VibeRule = class extends NestedAgentsmdRule {
78306
+ static getFamily() {
77025
78307
  return {
77026
- success: true,
77027
- error: null
77028
- };
77029
- }
77030
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
77031
- return new VibeRule({
77032
- outputRoot,
77033
- relativeDirPath,
77034
- relativeFilePath,
77035
- fileContent: "",
77036
- validate: false,
77037
- root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".vibe")
77038
- });
77039
- }
77040
- static isTargetedByRulesyncRule(rulesyncRule) {
77041
- return this.isTargetedByRulesyncRuleDefault({
77042
- rulesyncRule,
78308
+ globalDir: VIBE_GLOBAL_DIR,
77043
78309
  toolTarget: "vibe"
77044
- });
78310
+ };
77045
78311
  }
77046
78312
  };
77047
78313
  //#endregion
@@ -79248,6 +80514,16 @@ async function activateHermesProjectPlugins({ pluginNames, dryRun, logger }) {
79248
80514
  relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
79249
80515
  });
79250
80516
  const configPath = join(configRoot, relativeConfigPath);
80517
+ if (await refusesWriteOutsideRoot({
80518
+ logger,
80519
+ rootPath: configRoot,
80520
+ targetPath: configPath
80521
+ })) return {
80522
+ count: 0,
80523
+ paths: [],
80524
+ hasDiff: false,
80525
+ sourceLoadFailed: false
80526
+ };
79251
80527
  const expectedConfig = mergeEnabledPlugins({
79252
80528
  existingContent: await readFileContentOrNull(configPath) ?? "",
79253
80529
  filePath: configPath,
@@ -79715,7 +80991,8 @@ async function processEmptyFeatureGeneration(params) {
79715
80991
  */
79716
80992
  async function processFeatureWithRulesyncFiles(params) {
79717
80993
  const { config, processor, rulesyncFiles, sweepPlan, skipFilePaths, onToolFiles } = params;
79718
- if (rulesyncFiles.length === 0) return processEmptyFeatureGeneration({
80994
+ const retractsForEmptySource = processor.emitsToolFilesForEmptySource() && !processor.hasRulesyncSourceLoadFailure();
80995
+ if (rulesyncFiles.length === 0 && !retractsForEmptySource) return processEmptyFeatureGeneration({
79719
80996
  config,
79720
80997
  processor,
79721
80998
  sweepPlan,
@@ -80947,4 +82224,4 @@ async function importChecksCore(params) {
80947
82224
  //#endregion
80948
82225
  export { RulesyncCommand as $, toPosixPath as $t, CLAUDECODE_MEMORIES_DIR_NAME as A, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as An, checkPathTraversal as At, RulesyncSubagent as B, stripControlCharacters as Bn, listDirectoryEntryNames as Bt, COMMANDCODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, withWarnOnceScope as Ct, CODEXCLI_DIR as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, assertDirectoryIfExists as Dt, CODEXCLI_BASH_RULES_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, applyFileMode as Et, caseFoldIdentity as F, formatError as Fn, getFileSize as Ft, RulesyncRuleFrontmatterSchema as G, readFileContentOrNull as Gt, RulesyncSkill as H, stripHiddenCharacters as Hn, listSubdirectoryNames as Ht, groupSpellingsByCaseFoldedIdentity as I, truncateText as In, getHomeDirectory as It, RulesyncIgnore as J, removeFile as Jt, RulesyncPermissions as K, removeDirectory as Kt, AUGMENTCODE_DIR as L, hasDeceptiveHiddenCharacters as Ln, isFileNotFoundError as Lt, CLAUDECODE_SKILLS_DIR_PATH as M, ALL_FEATURES as Mn, directoryExists as Mt, FACTORYDROID_DIR as N, ALL_FEATURES_WITH_WILDCARD as Nn, ensureDir as Nt, CLAUDECODE_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, assertTreeContainsNoSymlinks as Ot, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, fileExists as Pt, parseJsonc as Q, runWithDirectoryRollback as Qt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as R, hasEnclosingMarkOutsideKeycap as Rn, isFileSystemError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, resetRunWarningState as St, ChecksProcessor as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, ErrorCodes as Tt, RulesyncSkillFrontmatterSchema as U, pathEscapesRoot as Ut, RulesyncSubagentFrontmatterSchema as V, stripControlCharactersKeepingLineFeeds as Vn, listFilePathsRecursively as Vt, RulesyncRule as W, readFileContent as Wt, getRulesyncSourceCandidates as X, removeTempDirectory as Xt, RulesyncHooks as Y, removeFileStrict as Yt, resolveRulesyncSourceWritePath as Z, resolvePath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, JsonLogger as _t, getProcessorRegistryEntry as a, ToolTargetSchema as an, SHARED_USER_MANAGED_CONFIG_PATHS as at, CommandsProcessor as b, RULESYNC_MCP_SCHEMA_URL as bn, warnOnConflictingFlags as bt, RulesProcessor as c, MAX_FILE_SIZE as cn, mergeInputRootConfigs as ct, CODEBUDDY_DIR as d, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as dn, ConfigFileSchema as dt, writeFileBuffer as en, RulesyncCommandFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CONFIG_RELATIVE_FILE_PATH as fn, GITIGNORE_DESTINATION_KEY as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, ConsoleLogger as gt, shortenToWidth as h, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as hn, findControlCharacter as ht, inspectInputRoots as i, PACKAGING_TOOL_TARGETS as in, loadYaml as it, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, createTempDirectory as jt, CLAUDECODE_LOCAL_RULE_FILE_NAME as k, RULESYNC_USER_CONFIG_DIR_NAME as kn, assertWritablePathInsideRoot as kt, SubagentsProcessor as l, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ln, resolveEffectiveInputRoots as lt, displayWidthOf as m, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as mn, assertTargetsFeaturesExclusive as mt, formatSourceLoadFailure as n, ALL_TOOL_TARGETS as nn, RulesyncCheckFrontmatterSchema as nt, convertFromTool as o, CURATED_RULES_FEATURE_SUBDIR as on, SKILL_FILE_NAME as ot, ELLIPSIS_WIDTH as p, RULESYNC_CONFIG_SCHEMA_URL as pn, SourceEntrySchema as pt, RulesyncMcp as q, removeDirectoryStrict as qt, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rn, stringifyFrontmatter as rt, isPackagingToolTarget as s, FEATURE_SOURCE_TREE_ENTRIES as sn, ConfigResolver as st, importFromTool as t, writeFileContent as tn, RulesyncCheck as tt, SkillsProcessor as u, RULESYNC_CHECKS_RELATIVE_DIR_PATH as un, CONFLICTING_TARGET_PAIRS as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, WarningCollectingLogger as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, CLIError as wt, QWENCODE_DIR as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, withFallbackLoggerTarget as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, fallbackLogger as yt, getLocalSkillDirNames as z, quoteForLog as zn, isSymlink as zt };
80949
82226
 
80950
- //# sourceMappingURL=import-BiyWNVZY.js.map
82227
+ //# sourceMappingURL=import-B2ANxi1V.js.map