agent-shim 8.18.0 → 8.20.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/README.md CHANGED
@@ -57,7 +57,7 @@ That's it — with no further configuration, everything in `~/.claude` that isn'
57
57
  - **An identity** is a login: the thing that owns credentials and daemon state, selected with `agent-shim run @<name>` or, once `agent-shim shim enable` has run, `claude @<name>`.
58
58
  - **A configuration profile** is a reusable, named bundle of sharing rules, independent of any identity, resolved per working directory via directory rules or a committed `.agent-shim.json`.
59
59
 
60
- A third, optional thing sits on top of identities: **a pool** is a named list of them. Select `pool:<name>` where an identity name goes (`claude @pool:subs`) and agent-shim picks the member whose unused quota is closest to expiring, skipping any that are refused right now, using the usage it already records. See [docs/configuration-model.md](docs/configuration-model.md#pools-picking-an-account-at-launch).
60
+ A third, optional thing sits on top of identities: **a pool** is a named list of them. Select `pool:<name>` where an identity name goes (`claude @pool:subs`) and agent-shim picks the member whose unused quota is closest to expiring, skipping any that are refused right now, using the usage it already records. A pool can instead declare `--preference listed`, which tries the members in the order the pool lists them, skipping only one that is refused right now. A member entry may itself name another pool (`pool:<name>`), which is ranked with its own policy and contributes its pick at that position, so "this identity first, then the fleet" nests the fleet rather than copying its member list; cycles are refused. See [docs/configuration-model.md](docs/configuration-model.md#pools-picking-an-account-at-launch).
61
61
 
62
62
  Every top-level entry in `~/.claude` is classified into one of five categories, shipped as a default map (`config/categories.default.json`):
63
63
 
@@ -103,8 +103,8 @@ agent-shim profile show <name> [--json]
103
103
  agent-shim profile remove <name> [--yes] [--json]
104
104
  agent-shim profile use <name> # the global default configuration profile [--json]
105
105
 
106
- agent-shim pool add <name> --identity <identity>... [--json]
107
- agent-shim pool set <name> --identity <identity>... [--json]
106
+ agent-shim pool add <name> --identity <member>... [--preference <score|listed>] [--json]
107
+ agent-shim pool set <name> [--identity <member>...] [--preference <score|listed> | --no-preference] [--json]
108
108
  agent-shim pool list [--json]
109
109
  agent-shim pool show <name> [--json]
110
110
  agent-shim pool remove <name> [--yes] [--json]
package/dist/cli.cjs CHANGED
@@ -55299,9 +55299,14 @@ var POOL_SELECTOR_PREFIX = "pool:";
55299
55299
  var POOL_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._-]*";
55300
55300
  var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
55301
55301
  var PoolNameSchema = external_exports.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
55302
- var IdentitySelectorSchema = external_exports.string().min(1).regex(new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`));
55302
+ var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
55303
+ var IdentitySelectorSchema = external_exports.string().min(1).regex(SELECTOR_RE);
55304
+ var PoolMemberSchema = external_exports.string().min(1).regex(SELECTOR_RE);
55305
+ var POOL_PREFERENCES = ["score", "listed"];
55306
+ var PoolPreferenceSchema = external_exports.enum(POOL_PREFERENCES);
55303
55307
  var PoolSchema = external_exports.strictObject({
55304
- identities: external_exports.array(external_exports.string().min(1).regex(new RegExp(`^${IDENTITY_NAME_PATTERN}$`))).min(1).refine((names) => new Set(names).size === names.length, { message: "pool members must be unique" })
55308
+ identities: external_exports.array(PoolMemberSchema).min(1).refine((members2) => new Set(members2).size === members2.length, { message: "pool members must be unique" }),
55309
+ preference: PoolPreferenceSchema.optional()
55305
55310
  });
55306
55311
  var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
55307
55312
  path: external_exports.string().min(1),
@@ -58305,7 +58310,41 @@ function requirePool(paths, name) {
58305
58310
  function writePools(paths, pools) {
58306
58311
  applyPatch(paths.globalConfigFile, GlobalConfigSchema, { pools }, { defaults: {} });
58307
58312
  }
58308
- function addPool(paths, name, identities) {
58313
+ function poolCycleOf(pools, name) {
58314
+ const walk = (current, path44) => {
58315
+ for (const entry of pools[current]?.identities ?? []) {
58316
+ const nested = poolNameOf(entry);
58317
+ if (nested === void 0) {
58318
+ continue;
58319
+ }
58320
+ if (nested === name) {
58321
+ return [...path44, nested];
58322
+ }
58323
+ if (path44.includes(nested)) {
58324
+ continue;
58325
+ }
58326
+ const found = walk(nested, [...path44, nested]);
58327
+ if (found !== void 0) {
58328
+ return found;
58329
+ }
58330
+ }
58331
+ return void 0;
58332
+ };
58333
+ return walk(name, [name]);
58334
+ }
58335
+ function validateMembers(pools, name, members2) {
58336
+ for (const entry of members2) {
58337
+ const nested = poolNameOf(entry);
58338
+ if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
58339
+ throw new UsageError(`Member "${entry}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
58340
+ }
58341
+ }
58342
+ const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members2] } }, name);
58343
+ if (cycle !== void 0) {
58344
+ throw new UsageError(`Member "pool:${cycle[1] ?? name}" would make pool "${name}" reach itself: ${cycle.join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
58345
+ }
58346
+ }
58347
+ function addPool(paths, name, identities, preference) {
58309
58348
  if (!PoolNameSchema.safeParse(name).success) {
58310
58349
  throw new InvalidPoolNameError(name);
58311
58350
  }
@@ -58313,13 +58352,15 @@ function addPool(paths, name, identities) {
58313
58352
  if (pools[name] !== void 0) {
58314
58353
  throw new PoolAlreadyExistsError(name);
58315
58354
  }
58316
- const pool = { identities: [...identities] };
58355
+ validateMembers(pools, name, identities);
58356
+ const pool = { identities: [...identities], ...preference === void 0 ? {} : { preference } };
58317
58357
  writePools(paths, { ...pools, [name]: pool });
58318
58358
  return pool;
58319
58359
  }
58320
- function setPool(paths, name, identities) {
58360
+ function setPool(paths, name, identities, preference) {
58321
58361
  requirePool(paths, name);
58322
- const pool = { identities: [...identities] };
58362
+ validateMembers(readPools(paths), name, identities);
58363
+ const pool = { identities: [...identities], ...preference === void 0 ? {} : { preference } };
58323
58364
  writePools(paths, { ...readPools(paths), [name]: pool });
58324
58365
  return pool;
58325
58366
  }
@@ -61749,7 +61790,7 @@ function registerIdentityCommand(program2, deps) {
61749
61790
  // package.json
61750
61791
  var package_default = {
61751
61792
  name: "agent-shim",
61752
- version: "8.18.0",
61793
+ version: "8.20.0",
61753
61794
  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.",
61754
61795
  license: "MIT",
61755
61796
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -61828,7 +61869,7 @@ var package_default = {
61828
61869
  rollup: "4.64.0",
61829
61870
  "rollup-plugin-dts": "6.5.1",
61830
61871
  "semantic-release": "^25.0.9",
61831
- turbo: "^2.10.12",
61872
+ turbo: "^2.11.5",
61832
61873
  typescript: "^6.0.3",
61833
61874
  "typescript-eslint": "^8.70.0",
61834
61875
  vitest: "^4.1.11"
@@ -62756,7 +62797,30 @@ function describeWindow(label, window, effective, nowMs) {
62756
62797
  function capacityOf(plan) {
62757
62798
  return plan.kind === "subscription" ? plan.capacity : 1;
62758
62799
  }
62800
+ function assessRefusedNested(member, nested, nowMs) {
62801
+ const refusal = nested.earliestReturn === void 0 ? { reason: `no member of pool "${nested.pool}" can be picked` } : { reason: `every member of pool "${nested.pool}" is refused; ${nested.earliestReturn.identity} returns at ${new Date(nested.earliestReturn.atMs).toISOString()} (in ${formatAge(nested.earliestReturn.atMs - nowMs)})`, blockedUntilMs: nested.earliestReturn.atMs };
62802
+ return {
62803
+ candidate: {
62804
+ identity: member.identity,
62805
+ class: "ineligible",
62806
+ ...refusal.blockedUntilMs === void 0 ? {} : { blockedUntilMs: refusal.blockedUntilMs },
62807
+ plan: planOf(member.account),
62808
+ reasons: [refusal.reason]
62809
+ }
62810
+ };
62811
+ }
62759
62812
  function assess(member, nowMs) {
62813
+ const nested = member.nested;
62814
+ if (nested === void 0) {
62815
+ return assessIdentity(member, nowMs);
62816
+ }
62817
+ if (nested.kind === "refused") {
62818
+ return assessRefusedNested(member, nested, nowMs);
62819
+ }
62820
+ const assessment = assessIdentity(member, nowMs);
62821
+ return { ...assessment, candidate: { ...assessment.candidate, reasons: [`picked by pool "${nested.pool}": ${nested.reasons.join("; ")}`, ...assessment.candidate.reasons] } };
62822
+ }
62823
+ function assessIdentity(member, nowMs) {
62760
62824
  const plan = planOf(member.account);
62761
62825
  const planReasons = plan.kind === "pay-per-use" ? ["billed by use (no plan allowance)"] : plan.recognised ? [`${String(plan.capacity)}x plan`] : [`plan tier ${plan.tier === void 0 ? "not known" : `"${plan.tier}" not recognised`}, counted as 1x`];
62762
62826
  const base = { identity: member.identity, plan };
@@ -62823,7 +62887,7 @@ function rankPool(input2) {
62823
62887
  }
62824
62888
  return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
62825
62889
  });
62826
- const ordered = [...candidates].sort(
62890
+ const ordered = input2.preference === "listed" ? [...candidates] : [...candidates].sort(
62827
62891
  (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)
62828
62892
  );
62829
62893
  const sticky = input2.sticky;
@@ -63108,6 +63172,12 @@ function createUsageStore(deps) {
63108
63172
  }
63109
63173
 
63110
63174
  // src/usage/poolPick.ts
63175
+ var PoolGraphError = class extends CliError {
63176
+ constructor(message) {
63177
+ super(message);
63178
+ this.name = "PoolGraphError";
63179
+ }
63180
+ };
63111
63181
  var SCHEMA_VERSION = 1;
63112
63182
  var PickEntrySchema = external_exports.strictObject({ identity: external_exports.string().min(1), at: external_exports.iso.datetime() });
63113
63183
  var PicksFileSchema = external_exports.strictObject({
@@ -63134,13 +63204,63 @@ function loadPoolMembers(fs19, paths, identities, nowMs) {
63134
63204
  }
63135
63205
  });
63136
63206
  }
63137
- function rankPoolFromStore(params) {
63138
- return rankPool({
63139
- members: loadPoolMembers(params.fs, params.paths, params.identities, params.nowMs),
63140
- nowMs: params.nowMs,
63141
- resuming: params.resuming,
63142
- ...params.sticky === void 0 ? {} : { sticky: params.sticky }
63207
+ function rankPoolGraph(input2) {
63208
+ return rankNamedPool(input2, input2.poolName, [], input2.sticky);
63209
+ }
63210
+ function rankNamedPool(input2, poolName, stack, sticky) {
63211
+ const pool = input2.pools[poolName];
63212
+ if (pool === void 0) {
63213
+ throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
63214
+ }
63215
+ const childStack = [...stack, poolName];
63216
+ const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
63217
+ const missing = direct.filter((entry) => !input2.identityExists(entry)).map((identity) => ({ pool: poolName, identity }));
63218
+ const loaded = new Map(loadPoolMembers(input2.fs, input2.paths, direct.filter(input2.identityExists), input2.nowMs).map((member) => [member.identity, member]));
63219
+ const members2 = [];
63220
+ for (const entry of pool.identities) {
63221
+ const nestedName = poolNameOf(entry);
63222
+ if (nestedName === void 0) {
63223
+ const member = loaded.get(entry);
63224
+ if (member !== void 0) {
63225
+ members2.push(member);
63226
+ }
63227
+ continue;
63228
+ }
63229
+ if (input2.pools[nestedName] === void 0) {
63230
+ throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
63231
+ }
63232
+ if (childStack.includes(nestedName)) {
63233
+ 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.`);
63234
+ }
63235
+ const nested = nestedMember(input2, nestedName, childStack, sticky);
63236
+ members2.push(nested.member);
63237
+ missing.push(...nested.missing);
63238
+ }
63239
+ const ranking = rankPool({
63240
+ members: members2,
63241
+ nowMs: input2.nowMs,
63242
+ resuming: input2.resuming,
63243
+ ...sticky === void 0 ? {} : { sticky },
63244
+ ...pool.preference === void 0 ? {} : { preference: pool.preference }
63143
63245
  });
63246
+ return { ranking, missing };
63247
+ }
63248
+ function nestedMember(input2, nestedName, childStack, sticky) {
63249
+ const nested = rankNamedPool(input2, nestedName, childStack, sticky);
63250
+ const pick2 = nested.ranking.pick;
63251
+ if (pick2 !== void 0) {
63252
+ const [member] = loadPoolMembers(input2.fs, input2.paths, [pick2.identity], input2.nowMs);
63253
+ return { member: { ...member ?? { identity: pick2.identity, records: [] }, nested: { kind: "pick", pool: nestedName, reasons: pick2.reasons } }, missing: nested.missing };
63254
+ }
63255
+ const earliestReturn = nested.ranking.earliestReturn;
63256
+ return {
63257
+ member: {
63258
+ identity: earliestReturn?.identity ?? `pool:${nestedName}`,
63259
+ records: [],
63260
+ nested: { kind: "refused", pool: nestedName, ...earliestReturn === void 0 ? {} : { earliestReturn } }
63261
+ },
63262
+ missing: nested.missing
63263
+ };
63144
63264
  }
63145
63265
  function readPicksFile(fs19, file2) {
63146
63266
  const empty = { schemaVersion: SCHEMA_VERSION, picks: {} };
@@ -63171,59 +63291,6 @@ function recordStickyPick(fs19, picksFile, directory, identity, nowMs) {
63171
63291
  `);
63172
63292
  }
63173
63293
 
63174
- // src/launcher/pool.ts
63175
- var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
63176
- var TERMINATOR = "--";
63177
- var REASONS_LOGGED = 3;
63178
- function splitMembers(pool, paths, fs19) {
63179
- const exists = (name) => loadIdentity(paths.identitiesDir, name, fs19) !== void 0;
63180
- return { present: pool.identities.filter(exists), missing: pool.identities.filter((name) => !exists(name)) };
63181
- }
63182
- function isResuming(passthrough) {
63183
- const end = passthrough.indexOf(TERMINATOR);
63184
- return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
63185
- }
63186
- function resolvePoolLaunch(params) {
63187
- const { poolName, log } = params;
63188
- const pool = params.pools?.[poolName];
63189
- if (pool === void 0) {
63190
- return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
63191
- }
63192
- const { present: identities, missing } = splitMembers(pool, params.paths, params.fs);
63193
- for (const name of missing) {
63194
- log.warn(`agent-shim: pool "${poolName}" names identity "${name}", which does not exist; skipping it`);
63195
- }
63196
- if (identities.length === 0) {
63197
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
63198
- }
63199
- const resuming = isResuming(params.passthrough);
63200
- const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
63201
- if (stickyRead.problem !== void 0) {
63202
- log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
63203
- }
63204
- for (; ; ) {
63205
- const nowMs = params.now();
63206
- const ranking = rankPoolFromStore({ fs: params.usageFs, paths: params.paths, identities, nowMs, resuming, ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky } });
63207
- const { pick: pick2, earliestReturn } = ranking;
63208
- if (pick2 !== void 0) {
63209
- recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick2.identity, nowMs);
63210
- return { ok: true, identity: pick2.identity, explanation: `pool ${poolName}: ${pick2.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
63211
- }
63212
- if (earliestReturn === void 0) {
63213
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
63214
- }
63215
- const until = new Date(earliestReturn.atMs).toISOString();
63216
- if (!params.wait) {
63217
- return {
63218
- ok: false,
63219
- message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
63220
- };
63221
- }
63222
- log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
63223
- params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
63224
- }
63225
- }
63226
-
63227
63294
  // src/poolPickReport.ts
63228
63295
  function candidateView(candidate) {
63229
63296
  return {
@@ -63237,14 +63304,15 @@ function candidateView(candidate) {
63237
63304
  };
63238
63305
  }
63239
63306
  function collectPoolPick(params) {
63240
- const { present, missing } = splitMembers(params.pool, params.paths, params.fs);
63241
63307
  const sticky = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.directory);
63242
- const ranking = rankPoolFromStore({
63308
+ const { ranking, missing } = rankPoolGraph({
63243
63309
  fs: params.usageFs,
63244
63310
  paths: params.paths,
63245
- identities: present,
63311
+ pools: params.pools,
63312
+ poolName: params.poolName,
63246
63313
  nowMs: params.nowMs,
63247
63314
  resuming: false,
63315
+ identityExists: (name) => loadIdentity(params.paths.identitiesDir, name, params.fs) !== void 0,
63248
63316
  ...sticky.sticky === void 0 ? {} : { sticky: sticky.sticky }
63249
63317
  });
63250
63318
  return {
@@ -63252,7 +63320,7 @@ function collectPoolPick(params) {
63252
63320
  directory: params.directory,
63253
63321
  ...ranking.pick === void 0 ? {} : { pick: ranking.pick.identity },
63254
63322
  candidates: ranking.candidates.map(candidateView),
63255
- missing: [...missing],
63323
+ missing: missing.map(({ identity }) => identity),
63256
63324
  ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
63257
63325
  ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
63258
63326
  };
@@ -64047,11 +64115,11 @@ function collectCheckReport(params) {
64047
64115
  });
64048
64116
  let poolPick;
64049
64117
  if (decidedIdentity.pool !== void 0) {
64050
- const pool = loaded.globalConfig?.pools?.[decidedIdentity.pool];
64051
- if (pool === void 0) {
64118
+ const pools = loaded.globalConfig?.pools ?? {};
64119
+ if (pools[decidedIdentity.pool] === void 0) {
64052
64120
  throw new PoolNotFoundError(decidedIdentity.pool);
64053
64121
  }
64054
- poolPick = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: decidedIdentity.pool, pool, directory: cwd, nowMs: Date.now() });
64122
+ poolPick = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: decidedIdentity.pool, pools, directory: cwd, nowMs: Date.now() });
64055
64123
  }
64056
64124
  const identityDecision = poolPick?.pick === void 0 ? decidedIdentity : { ...decidedIdentity, name: poolPick.pick };
64057
64125
  const loadedIdentity = identityDecision.name === void 0 ? void 0 : loadIdentity(paths.identitiesDir, identityDecision.name, realFsPort);
@@ -67940,13 +68008,34 @@ function runDoctor(params) {
67940
68008
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
67941
68009
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
67942
68010
  for (const [poolName, pool] of Object.entries(pools)) {
67943
- const missing = pool.identities.filter((member) => !validIdentityNames.has(member));
67944
- if (missing.length === 0) {
68011
+ const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
68012
+ const undefinedPools = pool.identities.flatMap((member) => {
68013
+ const nested = poolNameOf(member);
68014
+ return nested === void 0 || nested in pools ? [] : [nested];
68015
+ });
68016
+ if (missing.length === 0 && undefinedPools.length === 0) {
67945
68017
  push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
67946
68018
  } else {
67947
- push("pool", "fail", `Pool "${poolName}" names ${missing.map((member) => `identity "${member}"`).join(" and ")}, which ${missing.length === 1 ? "does" : "do"} not exist.`, poolName);
68019
+ const problems = [
68020
+ ...missing.map((member) => `identity "${member}", which does not exist`),
68021
+ ...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
68022
+ ];
68023
+ push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
67948
68024
  }
67949
68025
  }
68026
+ const reportedCycles = /* @__PURE__ */ new Set();
68027
+ for (const poolName of Object.keys(pools).sort()) {
68028
+ const chain = poolCycleOf(pools, poolName);
68029
+ if (chain === void 0) {
68030
+ continue;
68031
+ }
68032
+ const key = [...chain].slice(0, -1).sort().join(">");
68033
+ if (reportedCycles.has(key)) {
68034
+ continue;
68035
+ }
68036
+ reportedCycles.add(key);
68037
+ push("pool", "fail", `Pool cycle: ${chain.join(" -> ")}. A pool cannot nest itself, directly or through another pool.`, chain[0]);
68038
+ }
67950
68039
  const selectionExists = (selector) => {
67951
68040
  const poolName = poolNameOf(selector);
67952
68041
  return poolName === void 0 ? validIdentityNames.has(selector) : poolName in pools;
@@ -69238,7 +69327,7 @@ var PoolPickReportSchema = external_exports.strictObject({
69238
69327
  /** The member a launch would run as, absent when every member is refused. */
69239
69328
  pick: external_exports.string().optional(),
69240
69329
  candidates: external_exports.array(PoolPickCandidateViewSchema),
69241
- /** Members that name an identity that does not exist; a launch skips them. */
69330
+ /** Identity members anywhere in the pool's graph that do not exist; a launch skips them. */
69242
69331
  missing: external_exports.array(external_exports.string()),
69243
69332
  /** When nothing can be picked, the soonest any member returns. */
69244
69333
  earliestReturn: external_exports.strictObject({ identity: external_exports.string(), at: external_exports.iso.datetime() }).optional(),
@@ -75782,7 +75871,7 @@ var LAUNCHER_FLAG_NAMES = [
75782
75871
  ...BOOLEAN_FLAGS.flatMap(({ flag }) => [flag, `--no-${flag.slice(2)}`])
75783
75872
  ];
75784
75873
  var NATIVE_FLAG = "--native";
75785
- var TERMINATOR2 = "--";
75874
+ var TERMINATOR = "--";
75786
75875
  function matchValuedFlag(token) {
75787
75876
  for (const flag of VALUED_FLAGS) {
75788
75877
  if (token === flag) {
@@ -75824,7 +75913,7 @@ function parseLauncherArgv(argv) {
75824
75913
  if (token === void 0) {
75825
75914
  continue;
75826
75915
  }
75827
- if (token === TERMINATOR2) {
75916
+ if (token === TERMINATOR) {
75828
75917
  rest.push(...remaining.slice(index));
75829
75918
  break;
75830
75919
  }
@@ -75851,7 +75940,7 @@ function parseLauncherArgv(argv) {
75851
75940
  value2 = matched.inlineValue;
75852
75941
  } else {
75853
75942
  const next = remaining[index + 1];
75854
- if (next === void 0 || next === TERMINATOR2) {
75943
+ if (next === void 0 || next === TERMINATOR) {
75855
75944
  rest.push(token);
75856
75945
  continue;
75857
75946
  }
@@ -76708,7 +76797,7 @@ function formatPoolPick(report, nowMs) {
76708
76797
  function requireIdentities(paths, names) {
76709
76798
  const existing = new Set(listIdentities(paths).map((entry) => entry.name));
76710
76799
  for (const name of names) {
76711
- if (!existing.has(name)) {
76800
+ if (poolNameOf(name) === void 0 && !existing.has(name)) {
76712
76801
  throw new IdentityNotFoundError(name);
76713
76802
  }
76714
76803
  }
@@ -76719,40 +76808,48 @@ function membersOf(options) {
76719
76808
  }
76720
76809
  return options.identity;
76721
76810
  }
76811
+ var PREFERENCE_DESCRIPTION = "How a launch picks a member: score (the default) picks the member whose remaining quota expires soonest; listed tries the members in the order the pool lists them, skipping any that is refused right now.";
76722
76812
  function registerPoolCommand(program2, deps) {
76723
76813
  const { paths } = deps;
76724
76814
  const pool = withExamples(
76725
- program2.command("pool").description("Manage pools: named sets of identities that a launch picks one member of, by remaining quota, with `claude @pool:<name>`."),
76815
+ program2.command("pool").description("Manage pools: named sets of identities that a launch picks one member of, by remaining quota or the pool's listed order, with `claude @pool:<name>`."),
76726
76816
  ["agent-shim pool add subs --identity work --identity personal", "agent-shim pool pick subs"]
76727
76817
  );
76728
76818
  withExamples(
76729
- pool.command("add <name>").description("Define a pool. Fails if one with this name already exists.").option("--identity <name>", "A member identity (repeatable).", collectRepeated).option("--json", "Print the result as JSON.").action((name, options) => {
76819
+ pool.command("add <name>").description("Define a pool. Fails if one with this name already exists.").option("--identity <name>", "A member: an identity name, or pool:<name> to nest another pool with its own policy (repeatable).", collectRepeated).addOption(new Option("--preference <mode>", PREFERENCE_DESCRIPTION).choices(POOL_PREFERENCES)).option("--json", "Print the result as JSON.").action((name, options) => {
76730
76820
  const members2 = membersOf(options);
76731
76821
  requireIdentities(paths, members2);
76732
- const created = addPool(paths, name, members2);
76822
+ const created = addPool(paths, name, members2, options.preference);
76733
76823
  reportMutation(options.json, { action: "created", kind: "pool", name, value: created }, () => {
76734
76824
  console.log(`Created pool "${name}" with ${members2.join(", ")}.`);
76735
76825
  });
76736
76826
  }),
76737
- ["agent-shim pool add subs --identity work --identity personal"]
76827
+ ["agent-shim pool add subs --identity work --identity personal", "agent-shim pool add client --identity client-main --identity spare --preference listed"]
76738
76828
  );
76739
76829
  withExamples(
76740
- pool.command("set <name>").description("Replace a pool's members.").option("--identity <name>", "A member identity (repeatable); the full new list.", collectRepeated).option("--json", "Print the result as JSON.").action((name, options) => {
76741
- const members2 = membersOf(options);
76742
- requireIdentities(paths, members2);
76743
- const updated = setPool(paths, name, members2);
76830
+ pool.command("set <name>").description("Replace a pool's members, its preference, or both.").option("--identity <name>", "A member: an identity name, or pool:<name> to nest another pool with its own policy (repeatable); the full new list. Kept as-is when this option is absent.", collectRepeated).addOption(new Option("--preference <mode>", PREFERENCE_DESCRIPTION).choices(POOL_PREFERENCES)).option("--no-preference", "Clear the preference back to the default, score.").option("--json", "Print the result as JSON.").action((name, options) => {
76831
+ const existing = requirePool(paths, name);
76832
+ if (options.identity === void 0 && options.preference === void 0) {
76833
+ throw new UsageError("Nothing to change: pass --identity, --preference or --no-preference.");
76834
+ }
76835
+ const members2 = options.identity === void 0 ? existing.identities : membersOf(options);
76836
+ if (options.identity !== void 0) {
76837
+ requireIdentities(paths, members2);
76838
+ }
76839
+ const preference = options.preference === void 0 ? existing.preference : options.preference === false ? void 0 : options.preference;
76840
+ const updated = setPool(paths, name, members2, preference);
76744
76841
  reportMutation(options.json, { action: "updated", kind: "pool", name, value: updated }, () => {
76745
76842
  console.log(`Pool "${name}" now has ${members2.join(", ")}.`);
76746
76843
  });
76747
76844
  }),
76748
- ["agent-shim pool set subs --identity work --identity personal --identity spare"]
76845
+ ["agent-shim pool set subs --identity work --identity personal --identity spare", "agent-shim pool set subs --preference listed"]
76749
76846
  );
76750
76847
  withExamples(
76751
76848
  pool.command("list").description("List every pool and its members, marking the active selection with *.").option("--json", "Print the pools as JSON.").action((options) => {
76752
76849
  const pools = readPools(paths);
76753
76850
  const active = readActiveIdentity(paths);
76754
76851
  if (options.json === true) {
76755
- printJson(Object.entries(pools).map(([name, entry]) => ({ name, identities: entry.identities, active: active === `${POOL_SELECTOR_PREFIX}${name}` })));
76852
+ printJson(Object.entries(pools).map(([name, entry]) => ({ name, identities: entry.identities, ...entry.preference === void 0 ? {} : { preference: entry.preference }, active: active === `${POOL_SELECTOR_PREFIX}${name}` })));
76756
76853
  return;
76757
76854
  }
76758
76855
  const names = Object.keys(pools).sort();
@@ -76761,7 +76858,8 @@ function registerPoolCommand(program2, deps) {
76761
76858
  return;
76762
76859
  }
76763
76860
  for (const name of names) {
76764
- console.log(`${active === `${POOL_SELECTOR_PREFIX}${name}` ? "*" : " "} ${name}: ${(pools[name]?.identities ?? []).join(", ")}`);
76861
+ const entry = pools[name];
76862
+ console.log(`${active === `${POOL_SELECTOR_PREFIX}${name}` ? "*" : " "} ${name}: ${(entry?.identities ?? []).join(", ")}${entry?.preference === void 0 ? "" : " (preference: listed)"}`);
76765
76863
  }
76766
76864
  }),
76767
76865
  ["agent-shim pool list", "agent-shim pool list --json"]
@@ -76770,11 +76868,14 @@ function registerPoolCommand(program2, deps) {
76770
76868
  pool.command("show <name>").description("Show one pool's members.").option("--json", "Print the pool as JSON.").action((name, options) => {
76771
76869
  const entry = requirePool(paths, name);
76772
76870
  if (options.json === true) {
76773
- printJson({ name, identities: entry.identities });
76871
+ printJson({ name, identities: entry.identities, ...entry.preference === void 0 ? {} : { preference: entry.preference } });
76774
76872
  return;
76775
76873
  }
76776
76874
  console.log(`Pool: ${name}`);
76777
76875
  console.log(`Members: ${entry.identities.join(", ")}`);
76876
+ if (entry.preference !== void 0) {
76877
+ console.log(`Preference: ${entry.preference}`);
76878
+ }
76778
76879
  }),
76779
76880
  ["agent-shim pool show subs"]
76780
76881
  );
@@ -76801,7 +76902,7 @@ function registerPoolCommand(program2, deps) {
76801
76902
  withExamples(
76802
76903
  pool.command("pick <name>").description("Show how a launch from this directory would rank the pool's members right now, and which it would run as. Read-only: it records nothing.").option("--json", "Print the ranking as JSON.").action((name, options) => {
76803
76904
  const nowMs = Date.now();
76804
- const report = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: name, pool: requirePool(paths, name), directory: process.cwd(), nowMs });
76905
+ const report = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: name, pools: readPools(paths), directory: process.cwd(), nowMs });
76805
76906
  if (options.json === true) {
76806
76907
  printJson(report);
76807
76908
  return;
@@ -77568,6 +77669,76 @@ function splitExtraFlags(value2) {
77568
77669
  return trimmed.split(/\s+/);
77569
77670
  }
77570
77671
 
77672
+ // src/launcher/pool.ts
77673
+ var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
77674
+ var TERMINATOR2 = "--";
77675
+ var REASONS_LOGGED = 3;
77676
+ function isResuming(passthrough) {
77677
+ const end = passthrough.indexOf(TERMINATOR2);
77678
+ return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
77679
+ }
77680
+ function resolvePoolLaunch(params) {
77681
+ const { poolName, log } = params;
77682
+ const pool = params.pools?.[poolName];
77683
+ if (pool === void 0) {
77684
+ return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
77685
+ }
77686
+ const resuming = isResuming(params.passthrough);
77687
+ const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
77688
+ if (stickyRead.problem !== void 0) {
77689
+ log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
77690
+ }
77691
+ const warned = /* @__PURE__ */ new Set();
77692
+ for (; ; ) {
77693
+ const nowMs = params.now();
77694
+ let graph;
77695
+ try {
77696
+ graph = rankPoolGraph({
77697
+ fs: params.usageFs,
77698
+ paths: params.paths,
77699
+ pools: params.pools ?? {},
77700
+ poolName,
77701
+ nowMs,
77702
+ resuming,
77703
+ identityExists: (name) => loadIdentity(params.paths.identitiesDir, name, params.fs) !== void 0,
77704
+ ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky }
77705
+ });
77706
+ } catch (error62) {
77707
+ if (error62 instanceof PoolGraphError) {
77708
+ return { ok: false, message: `agent-shim: ${error62.message}` };
77709
+ }
77710
+ throw error62;
77711
+ }
77712
+ for (const gone of graph.missing) {
77713
+ const key = `${gone.pool}:${gone.identity}`;
77714
+ if (!warned.has(key)) {
77715
+ warned.add(key);
77716
+ log.warn(`agent-shim: pool "${gone.pool}" names identity "${gone.identity}", which does not exist; skipping it`);
77717
+ }
77718
+ }
77719
+ if (graph.ranking.candidates.length === 0) {
77720
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
77721
+ }
77722
+ const { pick: pick2, earliestReturn } = graph.ranking;
77723
+ if (pick2 !== void 0) {
77724
+ recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick2.identity, nowMs);
77725
+ return { ok: true, identity: pick2.identity, explanation: `pool ${poolName}: ${pick2.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
77726
+ }
77727
+ if (earliestReturn === void 0) {
77728
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
77729
+ }
77730
+ const until = new Date(earliestReturn.atMs).toISOString();
77731
+ if (!params.wait) {
77732
+ return {
77733
+ ok: false,
77734
+ message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
77735
+ };
77736
+ }
77737
+ log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
77738
+ params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
77739
+ }
77740
+ }
77741
+
77571
77742
  // src/launcher/spawn.ts
77572
77743
  var import_node_os7 = __toESM(require("node:os"), 1);
77573
77744
  var SIGNAL_EXIT_CODE_OFFSET = 128;