agent-shim 8.32.0 → 8.34.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.
package/dist/index.cjs CHANGED
@@ -368,6 +368,7 @@ var categories_default_default = {
368
368
  // src/config/schema.ts
369
369
  var import_node_path2 = __toESM(require("node:path"), 1);
370
370
  var import_zod = require("zod");
371
+ var import_trilean = require("trilean");
371
372
 
372
373
  // src/versionDiscovery.ts
373
374
  var import_node_os = __toESM(require("node:os"), 1);
@@ -498,13 +499,14 @@ var CategoryMapSchema = import_zod.z.strictObject({
498
499
  }).transform((input) => expandAllCategoryKey(input));
499
500
  var DURATION_RE = /^(?:0|[1-9][0-9]*)(?:ms|s|m|h|d|w)$/;
500
501
  var DurationSchema = import_zod.z.string().regex(DURATION_RE);
501
- var WhenSchema = import_zod.z.strictObject({
502
+ var WhenConditionObjectSchema = import_zod.z.strictObject({
502
503
  newerThan: DurationSchema.optional(),
503
504
  olderThan: DurationSchema.optional(),
504
505
  maxSizeBytes: import_zod.z.int().positive().optional(),
505
506
  branch: import_zod.z.string().min(1).optional(),
506
507
  env: import_zod.z.record(import_zod.z.string().min(1), import_zod.z.string()).optional()
507
508
  });
509
+ var WhenSchema = import_zod.z.union([WhenConditionObjectSchema, import_trilean.PredicateNodeSchema]);
508
510
  var EntryValueSchema = import_zod.z.union([
509
511
  import_zod.z.boolean(),
510
512
  import_zod.z.strictObject({ value: import_zod.z.boolean(), when: WhenSchema })
@@ -621,11 +623,14 @@ var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
621
623
  var PoolNameSchema = import_zod.z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
622
624
  var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
623
625
  var IdentitySelectorSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
624
- var PoolMemberSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
626
+ var PoolMemberSchema = import_zod.z.union([import_zod.z.string().min(1).regex(SELECTOR_RE), import_zod.z.strictObject({ identity: import_zod.z.string().min(1).regex(SELECTOR_RE), when: WhenSchema })]);
627
+ function poolMemberIdentity(member) {
628
+ return typeof member === "string" ? member : member.identity;
629
+ }
625
630
  var POOL_PREFERENCES = ["score", "listed"];
626
631
  var PoolPreferenceSchema = import_zod.z.enum(POOL_PREFERENCES);
627
632
  var PoolSchema = import_zod.z.strictObject({
628
- identities: import_zod.z.array(PoolMemberSchema).min(1).refine((members) => new Set(members).size === members.length, { message: "pool members must be unique" }),
633
+ identities: import_zod.z.array(PoolMemberSchema).min(1).refine((members) => new Set(members.map(poolMemberIdentity)).size === members.length, { message: "pool members must be unique" }),
629
634
  preference: PoolPreferenceSchema.optional()
630
635
  });
631
636
  var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
@@ -969,6 +974,7 @@ function classifyEntries(names, options) {
969
974
 
970
975
  // src/resolve/conditions.ts
971
976
  var import_picomatch2 = __toESM(require("picomatch"), 1);
977
+ var import_trilean2 = require("trilean");
972
978
  var MILLISECONDS_PER_UNIT = Object.freeze({
973
979
  ms: 1,
974
980
  s: 1e3,
@@ -999,52 +1005,87 @@ function matchBranch(pattern, branch, detached = false) {
999
1005
  }
1000
1006
  return (0, import_picomatch2.default)(pattern, { dot: true, nocase: false })(branch);
1001
1007
  }
1008
+ var evaluator = (0, import_trilean2.createSyncEvaluator)({});
1009
+ function resolversOf(context) {
1010
+ const pool = context.pool;
1011
+ const values = {
1012
+ now: context.nowMs,
1013
+ ...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
1014
+ ...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
1015
+ ...context.branch === void 0 ? {} : { "repo.branch": context.branch },
1016
+ "repo.detached": context.branchDetached ?? false,
1017
+ ...pool?.fiveHour === void 0 ? {} : { "quota.fiveHour.remaining": pool.fiveHour.remaining, "quota.fiveHour.utilization": pool.fiveHour.utilization, "quota.fiveHour.hoursUntilReset": pool.fiveHour.hoursUntilReset },
1018
+ ...pool?.sevenDay === void 0 ? {} : { "quota.sevenDay.remaining": pool.sevenDay.remaining, "quota.sevenDay.utilization": pool.sevenDay.utilization, "quota.sevenDay.hoursUntilReset": pool.sevenDay.hoursUntilReset },
1019
+ ...pool?.burnPerHour === void 0 ? {} : { "quota.burnPerHour": pool.burnPerHour },
1020
+ ...pool?.resuming === void 0 ? {} : { "session.resuming": pool.resuming },
1021
+ ...pool?.lastPickHoursAgo === void 0 ? {} : { "session.lastPickHoursAgo": pool.lastPickHoursAgo }
1022
+ };
1023
+ return {
1024
+ resolveValue: (key) => {
1025
+ const wrap = (value2) => ({ found: true, value: typeof value2 === "number" ? { kind: "number", value: value2 } : typeof value2 === "string" ? { kind: "text", value: value2 } : { kind: "boolean", value: value2 } });
1026
+ if (typeof key !== "string") {
1027
+ return { found: false };
1028
+ }
1029
+ if (key.startsWith("env.")) {
1030
+ const value2 = context.env[key.slice("env.".length)];
1031
+ return value2 === void 0 ? { found: false } : wrap(value2);
1032
+ }
1033
+ const value = values[key];
1034
+ return value === void 0 ? { found: false } : wrap(value);
1035
+ },
1036
+ // This embedding provides the reference namespace only: a tree that asks for a lookup or a collection finds neither, which the evaluator carries as indeterminate rather than letting the tree pretend an answer.
1037
+ resolveLookup: () => ({ found: false }),
1038
+ resolveCollection: () => []
1039
+ };
1040
+ }
1041
+ function fieldPredicate(field, when, context) {
1042
+ switch (field) {
1043
+ case "newerThan": {
1044
+ const window = parseDuration(when.newerThan ?? "");
1045
+ return { kind: "compare", op: "gte", left: { kind: "reference", key: "entry.latestMtimeMs" }, right: { kind: "numberLiteral", value: context.nowMs - window } };
1046
+ }
1047
+ case "olderThan": {
1048
+ const window = parseDuration(when.olderThan ?? "");
1049
+ return { kind: "compare", op: "lt", left: { kind: "reference", key: "entry.latestMtimeMs" }, right: { kind: "numberLiteral", value: context.nowMs - window } };
1050
+ }
1051
+ case "maxSizeBytes":
1052
+ return { kind: "compare", op: "lte", left: { kind: "reference", key: "entry.totalSizeBytes" }, right: { kind: "numberLiteral", value: when.maxSizeBytes ?? 0 } };
1053
+ case "branch":
1054
+ return { kind: "allOf", operands: [{ kind: "textCompare", op: "portableMatches", left: { kind: "reference", key: "repo.branch" }, right: { kind: "textLiteral", value: when.branch ?? "" } }, { kind: "compare", op: "eq", left: { kind: "reference", key: "repo.detached" }, right: { kind: "booleanLiteral", value: false } }] };
1055
+ case "env": {
1056
+ const entries = Object.entries(when.env ?? {});
1057
+ return { kind: "allOf", operands: entries.map(([name, expected]) => ({ kind: "textCompare", op: "equals", left: { kind: "reference", key: `env.${name}` }, right: { kind: "textLiteral", value: expected } })) };
1058
+ }
1059
+ }
1060
+ return { kind: "allOf", operands: [] };
1061
+ }
1002
1062
  function evaluateWhen(when, context) {
1003
1063
  if (when === void 0) {
1004
- return { passed: true, checked: [], failed: [] };
1064
+ return { status: "definite", passed: true, checked: [], failed: [] };
1065
+ }
1066
+ if ("kind" in when) {
1067
+ const result = evaluator.evaluatePredicate(when, void 0, resolversOf(context));
1068
+ return result.status === "indeterminate" ? { status: "indeterminate", reason: result.reason.message, checked: ["predicate"] } : { status: "definite", passed: result.value, checked: ["predicate"], failed: result.value ? [] : ["predicate"] };
1005
1069
  }
1006
1070
  const checked = [];
1007
1071
  const failed = [];
1008
- if (when.newerThan !== void 0) {
1009
- checked.push("newerThan");
1010
- const window = parseDuration(when.newerThan);
1011
- const latest = context.fact?.latestMtimeMs;
1012
- if (latest === void 0 || context.nowMs - latest > window) {
1013
- failed.push("newerThan");
1014
- }
1015
- }
1016
- if (when.olderThan !== void 0) {
1017
- checked.push("olderThan");
1018
- const window = parseDuration(when.olderThan);
1019
- const latest = context.fact?.latestMtimeMs;
1020
- if (latest === void 0 || context.nowMs - latest <= window) {
1021
- failed.push("olderThan");
1022
- }
1023
- }
1024
- if (when.maxSizeBytes !== void 0) {
1025
- checked.push("maxSizeBytes");
1026
- const total = context.fact?.totalSizeBytes;
1027
- if (total === void 0 || total > when.maxSizeBytes) {
1028
- failed.push("maxSizeBytes");
1072
+ for (const field of ["newerThan", "olderThan", "maxSizeBytes", "branch", "env"]) {
1073
+ if (when[field] === void 0) {
1074
+ continue;
1029
1075
  }
1030
- }
1031
- if (when.branch !== void 0) {
1032
- checked.push("branch");
1033
- if (!matchBranch(when.branch, context.branch, context.branchDetached ?? false)) {
1034
- failed.push("branch");
1076
+ checked.push(field);
1077
+ const result = evaluator.evaluatePredicate(fieldPredicate(field, when, context), void 0, resolversOf(context));
1078
+ if (result.status === "indeterminate") {
1079
+ return { status: "indeterminate", reason: result.reason.message, checked };
1035
1080
  }
1036
- }
1037
- if (when.env !== void 0) {
1038
- checked.push("env");
1039
- const mismatch = Object.entries(when.env).some(([name, expected]) => context.env[name] !== expected);
1040
- if (mismatch) {
1041
- failed.push("env");
1081
+ if (!result.value) {
1082
+ failed.push(field);
1042
1083
  }
1043
1084
  }
1044
- return { passed: failed.length === 0, checked, failed };
1085
+ return { status: "definite", passed: failed.length === 0, checked, failed };
1045
1086
  }
1046
1087
  function isVacuousWhen(when) {
1047
- return when !== void 0 && Object.keys(when).length === 0;
1088
+ return when !== void 0 && !("kind" in when) && Object.keys(when).length === 0;
1048
1089
  }
1049
1090
 
1050
1091
  // src/resolve/match.ts
@@ -1415,10 +1456,10 @@ function selectRule(relPath, flattened, context) {
1415
1456
  ...context.branchDetached === void 0 ? {} : { branchDetached: context.branchDetached },
1416
1457
  env: context.env
1417
1458
  });
1418
- if (evaluation.passed) {
1459
+ if (evaluation.status === "definite" && evaluation.passed) {
1419
1460
  return { rule, eliminated };
1420
1461
  }
1421
- eliminated.push({ rule, failed: evaluation.failed });
1462
+ eliminated.push({ rule, failed: evaluation.status === "definite" ? evaluation.failed : [`${evaluation.checked.join(", ")} could not be decided: ${evaluation.reason}`] });
1422
1463
  }
1423
1464
  return { eliminated };
1424
1465
  }
@@ -1652,7 +1693,10 @@ function planFarm(params) {
1652
1693
  var ENTER_EVERYTHING = () => true;
1653
1694
  function readsSubtreeAggregates(rule) {
1654
1695
  const when = rule.when;
1655
- return when !== void 0 && (when.newerThan !== void 0 || when.olderThan !== void 0 || when.maxSizeBytes !== void 0);
1696
+ if (when === void 0) {
1697
+ return false;
1698
+ }
1699
+ return "kind" in when || when.newerThan !== void 0 || when.olderThan !== void 0 || when.maxSizeBytes !== void 0;
1656
1700
  }
1657
1701
  function hasUnboundedDepth(pattern) {
1658
1702
  return pattern.includes("**") || /[{(]/.test(pattern);
@@ -5355,7 +5399,7 @@ var import_undici = require("undici");
5355
5399
  // package.json
5356
5400
  var package_default = {
5357
5401
  name: "agent-shim",
5358
- version: "8.32.0",
5402
+ version: "8.34.0",
5359
5403
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5360
5404
  license: "MIT",
5361
5405
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -5411,6 +5455,7 @@ var package_default = {
5411
5455
  commander: "^15.0.0",
5412
5456
  cosmiconfig: "^10.0.1",
5413
5457
  picomatch: "^4.0.7",
5458
+ trilean: "1.7.1",
5414
5459
  undici: "7.29.1",
5415
5460
  zod: "^4.6.1"
5416
5461
  },
@@ -6646,12 +6691,48 @@ function assessIdentity(member, nowMs) {
6646
6691
  ...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
6647
6692
  };
6648
6693
  }
6694
+ function poolFactsOf(member, nowMs, resuming, sticky) {
6695
+ const unified = member.snapshot?.providers[ANTHROPIC_PROVIDER]?.rateLimit?.unified;
6696
+ const window = (which) => {
6697
+ const raw = unified?.[which];
6698
+ if (raw?.utilization === void 0) {
6699
+ return void 0;
6700
+ }
6701
+ if (raw.resetsAt === void 0) {
6702
+ return void 0;
6703
+ }
6704
+ return { remaining: Math.max(0, 1 - raw.utilization), utilization: raw.utilization, hoursUntilReset: Math.max(0, (Date.parse(raw.resetsAt) - nowMs) / MS_PER_HOUR) };
6705
+ };
6706
+ const burn = windowBurn(member.records, "fiveHour");
6707
+ return {
6708
+ ...window("fiveHour") === void 0 ? {} : { fiveHour: window("fiveHour") },
6709
+ ...window("sevenDay") === void 0 ? {} : { sevenDay: window("sevenDay") },
6710
+ ...burn === void 0 ? {} : { burnPerHour: burn * MS_PER_HOUR },
6711
+ resuming,
6712
+ ...sticky?.identity === member.identity ? { lastPickHoursAgo: Math.max(0, (nowMs - Date.parse(sticky.at)) / MS_PER_HOUR) } : {}
6713
+ };
6714
+ }
6649
6715
  function rankPool(input) {
6650
6716
  const { members, nowMs } = input;
6651
- const assessments = members.map((member) => assess(member, nowMs));
6717
+ const assessments = members.map((member) => ({ member, ...assess(member, nowMs) }));
6652
6718
  const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
6653
6719
  const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
6654
- const candidates = assessments.map((assessment) => {
6720
+ const guarded = assessments.map((assessment) => {
6721
+ const { candidate } = assessment;
6722
+ if (candidate.class === "ineligible" || assessment.member.policy === void 0) {
6723
+ return assessment;
6724
+ }
6725
+ const context = { nowMs, env: {}, pool: poolFactsOf(assessment.member, nowMs, input.resuming, input.sticky) };
6726
+ const verdict = evaluateWhen(assessment.member.policy, context);
6727
+ if (verdict.status === "indeterminate") {
6728
+ return { ...assessment, candidate: { ...candidate, policyDemoted: true, reasons: [`policy undecided (${verdict.checked.join(", ") === "predicate" ? "predicate" : verdict.checked.join(", ")}): ${verdict.reason}`, ...candidate.reasons] } };
6729
+ }
6730
+ if (verdict.passed) {
6731
+ return assessment;
6732
+ }
6733
+ return { ...assessment, candidate: { ...candidate, class: "ineligible", reasons: [`skipped by policy (${verdict.checked.join(", ")}: did not hold)`, ...candidate.reasons] } };
6734
+ });
6735
+ const candidates = guarded.map((assessment) => {
6655
6736
  const { candidate, fiveHour, burn } = assessment;
6656
6737
  const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
6657
6738
  const { sevenDay, sevenDayBurn } = assessment;
@@ -6677,7 +6758,7 @@ function rankPool(input) {
6677
6758
  return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
6678
6759
  });
6679
6760
  const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
6680
- (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
6761
+ (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(left.policyDemoted ?? false) - Number(right.policyDemoted ?? false) || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
6681
6762
  );
6682
6763
  const sticky = input.sticky;
6683
6764
  const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
@@ -6848,27 +6929,27 @@ function rankNamedPool(input, poolName, stack, sticky) {
6848
6929
  throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
6849
6930
  }
6850
6931
  const childStack = [...stack, poolName];
6851
- const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
6852
- const missing = direct.filter((entry) => !input.identityExists(entry)).map((identity) => ({ pool: poolName, identity }));
6853
- const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter(input.identityExists), input.nowMs).map((member) => [member.identity, member]));
6932
+ const direct = pool.identities.filter((entry) => poolNameOf(poolMemberIdentity(entry)) === void 0);
6933
+ const missing = direct.filter((entry) => !input.identityExists(poolMemberIdentity(entry))).map((entry) => ({ pool: poolName, identity: poolMemberIdentity(entry) }));
6934
+ const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter((entry) => input.identityExists(poolMemberIdentity(entry))).map(poolMemberIdentity), input.nowMs).map((member) => [member.identity, member]));
6854
6935
  const members = [];
6855
6936
  for (const entry of pool.identities) {
6856
- const nestedName = poolNameOf(entry);
6937
+ const nestedName = poolNameOf(poolMemberIdentity(entry));
6857
6938
  if (nestedName === void 0) {
6858
- const member = loaded.get(entry);
6939
+ const member = loaded.get(poolMemberIdentity(entry));
6859
6940
  if (member !== void 0) {
6860
- members.push(member);
6941
+ members.push(typeof entry === "string" ? member : { ...member, policy: entry.when });
6861
6942
  }
6862
6943
  continue;
6863
6944
  }
6864
6945
  if (input.pools[nestedName] === void 0) {
6865
- throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
6946
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" names a pool that is not defined.`);
6866
6947
  }
6867
6948
  if (childStack.includes(nestedName)) {
6868
- throw new PoolGraphError(`Pool "${poolName}" member "${entry}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
6949
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
6869
6950
  }
6870
6951
  const nested = nestedMember(input, nestedName, childStack, sticky);
6871
- members.push(nested.member);
6952
+ members.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
6872
6953
  missing.push(...nested.missing);
6873
6954
  }
6874
6955
  const ranking = rankPool({
@@ -7129,7 +7210,7 @@ function writePools(paths, pools) {
7129
7210
  function poolCycleOf(pools, name) {
7130
7211
  const walk = (current, path32) => {
7131
7212
  for (const entry of pools[current]?.identities ?? []) {
7132
- const nested = poolNameOf(entry);
7213
+ const nested = poolNameOf(poolMemberIdentity(entry));
7133
7214
  if (nested === void 0) {
7134
7215
  continue;
7135
7216
  }
@@ -7150,9 +7231,10 @@ function poolCycleOf(pools, name) {
7150
7231
  }
7151
7232
  function validateMembers(pools, name, members) {
7152
7233
  for (const entry of members) {
7153
- const nested = poolNameOf(entry);
7234
+ const identity = poolMemberIdentity(entry);
7235
+ const nested = poolNameOf(identity);
7154
7236
  if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
7155
- throw new UsageError(`Member "${entry}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
7237
+ throw new UsageError(`Member "${identity}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
7156
7238
  }
7157
7239
  }
7158
7240
  const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members] } }, name);
@@ -9857,16 +9939,16 @@ function runDoctor(params) {
9857
9939
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
9858
9940
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
9859
9941
  for (const [poolName, pool] of Object.entries(pools)) {
9860
- const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
9942
+ const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
9861
9943
  const undefinedPools = pool.identities.flatMap((member) => {
9862
- const nested = poolNameOf(member);
9944
+ const nested = poolNameOf(poolMemberIdentity(member));
9863
9945
  return nested === void 0 || nested in pools ? [] : [nested];
9864
9946
  });
9865
9947
  if (missing.length === 0 && undefinedPools.length === 0) {
9866
- push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
9948
+ push("pool", "pass", `${poolName} is valid (${pool.identities.map(poolMemberIdentity).join(", ")}).`, poolName);
9867
9949
  } else {
9868
9950
  const problems = [
9869
- ...missing.map((member) => `identity "${member}", which does not exist`),
9951
+ ...missing.map((member) => `identity "${poolMemberIdentity(member)}", which does not exist`),
9870
9952
  ...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
9871
9953
  ];
9872
9954
  push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);