@shipster/cli 0.5.2 → 0.5.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +213 -111
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -18414,6 +18414,26 @@ function parseDesired(input) {
18414
18414
  }
18415
18415
  }
18416
18416
 
18417
+ // ../../shared/types/src/tier-plan.ts
18418
+ function ladder(rungs) {
18419
+ if (rungs === 3) return DIFFICULTIES.map((d) => ({ tier: tierForDifficulty(d), difficulty: [d] }));
18420
+ return [{ tier: "cheap", difficulty: ["small", "standard"] }, { tier: "sensitive", difficulty: ["large"] }];
18421
+ }
18422
+ function planWriterTiers(models) {
18423
+ if (models.some((m) => m.trim() === "")) throw new Error("writer models: an empty model name");
18424
+ if (new Set(models).size !== models.length) throw new Error("writer models: a repeated model");
18425
+ if (models.length > 3) throw new Error("writer models: at most three, one per difficulty");
18426
+ if (models.length < 2) return { pools: [{ tier: "all", ...models[0] ? { model: models[0] } : {} }], poolOrder: null };
18427
+ const pools = ladder(models.length).map((rung, i) => ({ tier: rung.tier, model: models[i], difficulty: rung.difficulty }));
18428
+ return { pools, poolOrder: pools.map((p) => p.tier) };
18429
+ }
18430
+ function planReviewerSeat(writer, available) {
18431
+ const other = available.find((r) => r !== writer.runtime);
18432
+ if (other !== void 0) return { runtime: other, separation: "runtime" };
18433
+ const top = writer.models.at(-1);
18434
+ return { runtime: writer.runtime, ...top ? { model: top } : {}, separation: writer.models.length > 1 ? "model" : "none" };
18435
+ }
18436
+
18417
18437
  // ../../shared/types/src/secret-patterns.ts
18418
18438
  var SECRET_PATTERNS = [
18419
18439
  /\bsk-/,
@@ -18473,6 +18493,14 @@ function isDifficulty(value) {
18473
18493
  function difficultyRung(d) {
18474
18494
  return DIFFICULTIES.indexOf(d);
18475
18495
  }
18496
+ var TIER_OF_DIFFICULTY = {
18497
+ small: "cheap",
18498
+ standard: "standard",
18499
+ large: "sensitive"
18500
+ };
18501
+ function tierForDifficulty(d) {
18502
+ return TIER_OF_DIFFICULTY[d];
18503
+ }
18476
18504
  var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
18477
18505
  var DEFAULT_REVIEW_SILENT_MS = 40 * 60 * 1e3;
18478
18506
  var POOL_STOPPED_REASON = "pool member stopped by pitboss node";
@@ -24883,95 +24911,12 @@ var DEFAULT_SESSION_MAX_MS = 90 * 6e4;
24883
24911
  var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
24884
24912
 
24885
24913
  // src/node/onboard.ts
24886
- import { join as join18, resolve as resolve6 } from "node:path";
24887
- var MAX_POOL = 30;
24888
- var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
24889
- function runtimeBinary(runtime, env3) {
24890
- switch (runtime) {
24891
- case "claude":
24892
- return env3.CLAUDE_BIN ?? "claude";
24893
- case "cursor":
24894
- return env3.CURSOR_BIN ?? "cursor-agent";
24895
- case "copilot":
24896
- return env3.COPILOT_BIN ?? "copilot";
24897
- case "codex":
24898
- return env3.CODEX_BIN ?? "codex";
24899
- }
24900
- }
24901
- function githubRepo(url) {
24902
- const match = GITHUB.exec(url.trim());
24903
- if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
24904
- return { owner: match[1], name: match[2] };
24905
- }
24906
- function planOnboard(url, opts) {
24907
- const { owner, name } = githubRepo(url);
24908
- for (const role of ["architect", "writer", "reviewer"]) {
24909
- if (opts[role] === void 0) throw new Error(`no ${role} runtime: pass --${role} <runtime> (have: ${RUNTIMES3.join(", ")})`);
24910
- }
24911
- const architectRuntime = opts.architect;
24912
- const writer = opts.writer;
24913
- const reviewer = opts.reviewer;
24914
- for (const r of [architectRuntime, writer, reviewer]) {
24915
- if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
24916
- }
24917
- const warnings = reviewer === writer ? [`the reviewer and the writers both run ${writer}: a same-vendor reviewer shares the writers' blind spots`] : [];
24918
- const poolMax = opts.poolMax ?? 2;
24919
- if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
24920
- throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
24921
- }
24922
- const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
24923
- const parent = resolve6(opts.parent ?? join18(opts.home, "github"));
24924
- const base2 = join18(parent, name);
24925
- const nodeDir2 = join18(parent, `${name}-node`);
24926
- const architectWorktree = join18(parent, `${name}-architect`);
24927
- const review = join18(parent, `${name}-reviewer`);
24928
- return {
24929
- owner,
24930
- name,
24931
- warnings,
24932
- cloneUrl: `https://github.com/${owner}/${name}.git`,
24933
- base: base2,
24934
- nodeDir: nodeDir2,
24935
- specPath: join18(nodeDir2, "spec.json"),
24936
- configDir: join18(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
24937
- worktrees: [architectWorktree, review],
24938
- spec: {
24939
- api: DEFAULT_API_URL,
24940
- agents: [
24941
- { name: `${id}-architect`, runtime: architectRuntime, worktree: `../${name}-architect`, specialization: "architect", tier: 2 },
24942
- { name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
24943
- {
24944
- pool: `${id}-pool`,
24945
- runtime: writer,
24946
- base: `../${name}`,
24947
- worktreeRoot: `../${name}-pool`,
24948
- specialization: "writer",
24949
- min: 0,
24950
- max: poolMax
24951
- }
24952
- ]
24953
- }
24954
- };
24955
- }
24956
- function onboardOperations(plan2, account) {
24957
- const oneLine2 = (v) => v.length > 0 && v.length <= 300 && !/[\r\n]/.test(v);
24958
- return {
24959
- ...account ? { githubAccount: account } : {},
24960
- ...oneLine2(plan2.configDir) ? { nodeConfigDir: plan2.configDir } : {},
24961
- ...oneLine2(plan2.specPath) ? { nodeSpecPath: plan2.specPath } : {}
24962
- };
24963
- }
24964
- function originHost(origin) {
24965
- const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
24966
- if (url) return url[1].toLowerCase();
24967
- const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
24968
- return scp ? scp[1].toLowerCase() : null;
24969
- }
24914
+ import { join as join19, resolve as resolve7 } from "node:path";
24970
24915
 
24971
24916
  // src/node/spec.ts
24972
24917
  import { existsSync as existsSync11, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
24973
24918
  import { homedir as homedir5 } from "node:os";
24974
- import { join as join19, parse as parsePath, resolve as resolve7, sep as sep2 } from "node:path";
24919
+ import { join as join18, parse as parsePath, resolve as resolve6, sep as sep2 } from "node:path";
24975
24920
  var MAX_POOL_SIZE = 32;
24976
24921
  var MAX_ENV_GRANTS2 = 16;
24977
24922
  var NAME2 = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
@@ -24984,7 +24929,7 @@ var canonical = (p) => {
24984
24929
  try {
24985
24930
  return realpathSync4(p);
24986
24931
  } catch {
24987
- return resolve7(p);
24932
+ return resolve6(p);
24988
24933
  }
24989
24934
  };
24990
24935
  function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
@@ -24995,7 +24940,7 @@ function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
24995
24940
  const skipped = [];
24996
24941
  raw.forEach((entry, i) => {
24997
24942
  if (typeof entry !== "string" || entry === "" || entry.includes("\0")) throw new Error(`janitorRoots[${i}] must be a directory path`);
24998
- const asked = resolve7(specDir, entry);
24943
+ const asked = resolve6(specDir, entry);
24999
24944
  let real3;
25000
24945
  try {
25001
24946
  real3 = realpathSync4(asked);
@@ -25010,7 +24955,7 @@ function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
25010
24955
  if (real3 === parsePath(real3).root) throw new Error(`janitorRoots[${i}] ${real3} is the filesystem root`);
25011
24956
  if (real3 === home) throw new Error(`janitorRoots[${i}] ${real3} is the home directory`);
25012
24957
  if (isWithin(home, real3)) throw new Error(`janitorRoots[${i}] ${real3} contains the home directory`);
25013
- if (existsSync11(join19(real3, ".git"))) throw new Error(`janitorRoots[${i}] ${real3} is a git work tree; list the directory that holds worktrees, not a checkout`);
24958
+ if (existsSync11(join18(real3, ".git"))) throw new Error(`janitorRoots[${i}] ${real3} is a git work tree; list the directory that holds worktrees, not a checkout`);
25014
24959
  for (const a of allAgents({ api: "", agents, pools })) {
25015
24960
  if (isWithin(real3, canonical(a.worktree))) {
25016
24961
  throw new Error(`janitorRoots[${i}] ${real3} is or sits inside the worktree of agent "${a.name}"`);
@@ -25044,7 +24989,7 @@ function poolMembers(pool) {
25044
24989
  return {
25045
24990
  name,
25046
24991
  runtime: pool.runtime,
25047
- worktree: join19(pool.worktreeRoot, name),
24992
+ worktree: join18(pool.worktreeRoot, name),
25048
24993
  specialization: pool.specialization,
25049
24994
  ...pool.ownerPool ? { ownerPool: true } : {},
25050
24995
  ...pool.tier ? { tier: pool.tier } : {},
@@ -25206,11 +25151,11 @@ function parsePool(a, i, specDir, platform2) {
25206
25151
  }
25207
25152
  burstMax = a.burstMax;
25208
25153
  }
25209
- const worktreeRoot = resolve7(specDir, a.worktreeRoot);
25154
+ const worktreeRoot = resolve6(specDir, a.worktreeRoot);
25210
25155
  if (ownerPool) {
25211
25156
  return { pool: a.pool, runtime, ownerPool: true, worktreeRoot, specialization, ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...poolSeatKindOf(a, i, platform2, max), ...difficultyOf(a, i), min, max, ...burstMax !== void 0 ? { burstMax } : {} };
25212
25157
  }
25213
- const base2 = resolve7(specDir, a.base);
25158
+ const base2 = resolve6(specDir, a.base);
25214
25159
  if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
25215
25160
  return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...poolSeatKindOf(a, i, platform2, max), ...difficultyOf(a, i), min, max, ...burstMax !== void 0 ? { burstMax } : {} };
25216
25161
  }
@@ -25247,7 +25192,7 @@ function parseSpec(text, specDir, opts = {}) {
25247
25192
  }
25248
25193
  const specialization = specializationOf(a, i);
25249
25194
  if (ownerPool) ownerPoolSpecializationCheck(a.name, specialization, "agents");
25250
- agents.push({ name: a.name, runtime, worktree: resolve7(specDir, a.worktree), specialization, ...ownerPool ? { ownerPool: true } : tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...seatKindOf(a, i, platform2) });
25195
+ agents.push({ name: a.name, runtime, worktree: resolve6(specDir, a.worktree), specialization, ...ownerPool ? { ownerPool: true } : tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...seatKindOf(a, i, platform2) });
25251
25196
  });
25252
25197
  const entries = [
25253
25198
  ...agents.map((a) => ({ label: `agent "${a.name}"`, ownerPool: a.ownerPool === true, seated: a.runAs !== void 0 })),
@@ -25291,6 +25236,115 @@ function parseSpec(text, specDir, opts = {}) {
25291
25236
  };
25292
25237
  }
25293
25238
 
25239
+ // src/node/onboard.ts
25240
+ var MAX_POOL = 30;
25241
+ var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
25242
+ function runtimeBinary(runtime, env3) {
25243
+ switch (runtime) {
25244
+ case "claude":
25245
+ return env3.CLAUDE_BIN ?? "claude";
25246
+ case "cursor":
25247
+ return env3.CURSOR_BIN ?? "cursor-agent";
25248
+ case "copilot":
25249
+ return env3.COPILOT_BIN ?? "copilot";
25250
+ case "codex":
25251
+ return env3.CODEX_BIN ?? "codex";
25252
+ }
25253
+ }
25254
+ function githubRepo(url) {
25255
+ const match = GITHUB.exec(url.trim());
25256
+ if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
25257
+ return { owner: match[1], name: match[2] };
25258
+ }
25259
+ function planOnboard(url, opts) {
25260
+ const { owner, name } = githubRepo(url);
25261
+ for (const role of ["architect", "writer", "reviewer"]) {
25262
+ if (opts[role] === void 0) throw new Error(`no ${role} runtime: pass --${role} <runtime> (have: ${RUNTIMES3.join(", ")})`);
25263
+ }
25264
+ const architectRuntime = opts.architect;
25265
+ const writer = opts.writer;
25266
+ const reviewer = opts.reviewer;
25267
+ for (const r of [architectRuntime, writer, reviewer]) {
25268
+ if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
25269
+ }
25270
+ const sameVendor = `the reviewer and the writers both run ${writer}: a same-vendor reviewer shares the writers' blind spots`;
25271
+ const warnings = reviewer === writer ? [sameVendor] : [];
25272
+ const poolMax = opts.poolMax ?? 2;
25273
+ if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
25274
+ throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
25275
+ }
25276
+ const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
25277
+ const parent = resolve7(opts.parent ?? join19(opts.home, "github"));
25278
+ const base2 = join19(parent, name);
25279
+ const nodeDir2 = join19(parent, `${name}-node`);
25280
+ const architectWorktree = join19(parent, `${name}-architect`);
25281
+ const review = join19(parent, `${name}-reviewer`);
25282
+ const writerPool = { runtime: writer, base: `../${name}`, specialization: "writer", min: 0, max: poolMax };
25283
+ const architect = { name: `${id}-architect`, runtime: architectRuntime, worktree: `../${name}-architect`, specialization: "architect", tier: 2 };
25284
+ const tiers = opts.writerModels ? planWriterTiers(opts.writerModels) : null;
25285
+ const poolName = (tier) => `${id}-${tier === "all" ? "pool" : tier}`;
25286
+ if (tiers && tiers.pools.length * poolMax > MAX_POOL) {
25287
+ throw new Error(`${tiers.pools.length} writer pools of ${poolMax} do not fit one approval of ${MAX_POOL + 2} agents with the architect and reviewer; lower --pool-max`);
25288
+ }
25289
+ const seat = tiers ? planReviewerSeat({ runtime: writer, models: opts.writerModels }, [reviewer]) : null;
25290
+ const tierAgents = tiers && seat && [
25291
+ architect,
25292
+ ...tiers.pools.map((p) => ({
25293
+ pool: poolName(p.tier),
25294
+ ...writerPool,
25295
+ worktreeRoot: `../${name}-${p.tier === "all" ? "pool" : p.tier}`,
25296
+ ...p.model ? { model: p.model } : {},
25297
+ ...p.difficulty ? { difficulty: p.difficulty } : {}
25298
+ })),
25299
+ {
25300
+ pool: `${id}-reviewer`,
25301
+ runtime: seat.runtime,
25302
+ base: `../${name}`,
25303
+ worktreeRoot: `../${name}-reviewer-pool`,
25304
+ specialization: "reviewer",
25305
+ ...seat.model ? { model: seat.model } : {},
25306
+ min: 0,
25307
+ max: 1
25308
+ }
25309
+ ];
25310
+ const spec = {
25311
+ api: DEFAULT_API_URL,
25312
+ agents: tierAgents || [
25313
+ architect,
25314
+ { name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
25315
+ { pool: `${id}-pool`, ...writerPool, worktreeRoot: `../${name}-pool` }
25316
+ ]
25317
+ };
25318
+ parseSpec(JSON.stringify(spec), nodeDir2);
25319
+ return {
25320
+ owner,
25321
+ name,
25322
+ warnings,
25323
+ cloneUrl: `https://github.com/${owner}/${name}.git`,
25324
+ base: base2,
25325
+ nodeDir: nodeDir2,
25326
+ specPath: join19(nodeDir2, "spec.json"),
25327
+ configDir: join19(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
25328
+ worktrees: tierAgents ? [architectWorktree] : [architectWorktree, review],
25329
+ poolOrder: tiers?.poolOrder?.map(poolName) ?? null,
25330
+ spec
25331
+ };
25332
+ }
25333
+ function onboardOperations(plan2, account) {
25334
+ const oneLine2 = (v) => v.length > 0 && v.length <= 300 && !/[\r\n]/.test(v);
25335
+ return {
25336
+ ...account ? { githubAccount: account } : {},
25337
+ ...oneLine2(plan2.configDir) ? { nodeConfigDir: plan2.configDir } : {},
25338
+ ...oneLine2(plan2.specPath) ? { nodeSpecPath: plan2.specPath } : {}
25339
+ };
25340
+ }
25341
+ function originHost(origin) {
25342
+ const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
25343
+ if (url) return url[1].toLowerCase();
25344
+ const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
25345
+ return scp ? scp[1].toLowerCase() : null;
25346
+ }
25347
+
25294
25348
  // src/node/store.ts
25295
25349
  import { existsSync as existsSync12, mkdirSync as mkdirSync10, readdirSync as readdirSync4, readFileSync as readFileSync15, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "node:fs";
25296
25350
  import { dirname as dirname10, join as join20 } from "node:path";
@@ -25392,7 +25446,7 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
25392
25446
  };
25393
25447
  for (const m of stopped) {
25394
25448
  if (count >= max) break;
25395
- if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
25449
+ if (m.inProgress > 0 || m.readyAssigned > 0 || (m.owedReviews ?? 0) > 0) take(m);
25396
25450
  }
25397
25451
  const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
25398
25452
  let demand = pending - running.filter(isIdle).length;
@@ -25415,7 +25469,7 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
25415
25469
  count--;
25416
25470
  }
25417
25471
  if (stop.length > 0) return { start: [], stop, why };
25418
- const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0).length;
25472
+ const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0 || (m.owedReviews ?? 0) > 0).length;
25419
25473
  const freed = [];
25420
25474
  if (blockedOwners > 0 && count >= max) {
25421
25475
  for (const m of running.filter(isIdle)) {
@@ -25891,7 +25945,8 @@ async function onboardCommand(url, opts) {
25891
25945
  architect: roles.architect,
25892
25946
  writer: roles.writer,
25893
25947
  reviewer: roles.reviewer,
25894
- poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax)
25948
+ poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax),
25949
+ writerModels: opts.writerModels === void 0 ? void 0 : opts.writerModels.split(",").map((m) => m.trim())
25895
25950
  });
25896
25951
  for (const warning of plan2.warnings) console.log(source_default.yellow(` ${warning}`));
25897
25952
  const env3 = { ...process.env };
@@ -25970,6 +26025,8 @@ async function onboardCommand(url, opts) {
25970
26025
  Start it when you want it running (long-running, foreground):`);
25971
26026
  console.log(source_default.cyan(` cd ${shellQuote(plan2.nodeDir)} && PITBOSS_CONFIG_DIR=${shellQuote(plan2.configDir)} pitboss node up spec.json`));
25972
26027
  if (opts.account) console.log(source_default.dim(` with GH_TOKEN from \`gh auth token --user ${opts.account}\` in that shell`));
26028
+ if (plan2.poolOrder) console.log(`
26029
+ Tiers route once the owner or its top-level agent sets each pool's difficulties (set_pool_size) and the pool order (set_pool_order): ${plan2.poolOrder.join(", ")}`);
25973
26030
  } catch (err) {
25974
26031
  console.error(source_default.red(` \u2715 ${err instanceof Error ? err.message : String(err)}`));
25975
26032
  process.exitCode = 1;
@@ -31620,6 +31677,28 @@ function startActivityLoop(agents, sup, ownerPool = false) {
31620
31677
  const timer = setInterval(() => void tick(), ACTIVITY_REPORT_MS);
31621
31678
  return () => clearInterval(timer);
31622
31679
  }
31680
+ var SHUTDOWN_MARK_BOUND_MS = 3e3;
31681
+ async function markPoolsStopped(loops, boundMs = SHUTDOWN_MARK_BOUND_MS) {
31682
+ let timer;
31683
+ const bound = new Promise((resolve19) => {
31684
+ timer = setTimeout(() => {
31685
+ console.log(`${(/* @__PURE__ */ new Date()).toISOString()} node: shutdown marking pool members unavailable timed out after ${boundMs}ms`);
31686
+ resolve19();
31687
+ }, boundMs);
31688
+ });
31689
+ try {
31690
+ await Promise.race([Promise.all(loops.map((l) => l.markRunningStopped().catch(() => void 0))), bound]);
31691
+ } finally {
31692
+ clearTimeout(timer);
31693
+ }
31694
+ }
31695
+ async function shutdownNode(opts) {
31696
+ for (const stop of opts.stops) stop();
31697
+ console.log(source_default.dim("\n stopping agents\u2026"));
31698
+ const marking = markPoolsStopped(opts.poolLoops, opts.boundMs);
31699
+ await Promise.all([opts.sup.stop(), marking]);
31700
+ opts.exit(0);
31701
+ }
31623
31702
  function startPoolLoop(run4, sup, record, account) {
31624
31703
  const { pool, members } = run4;
31625
31704
  const { reconcileMs, idleStopMs } = poolTimings();
@@ -31640,6 +31719,7 @@ function startPoolLoop(run4, sup, record, account) {
31640
31719
  }
31641
31720
  };
31642
31721
  let busy = false;
31722
+ let closed = false;
31643
31723
  const markStopped = async (member) => {
31644
31724
  try {
31645
31725
  await setAvailable(member.creds, false);
@@ -31735,12 +31815,17 @@ function startPoolLoop(run4, sup, record, account) {
31735
31815
  if (pool.ownerPool) resetOwnerPoolSlot(member.agent.worktree, member.agent.runAs);
31736
31816
  else resetWorktree(member.agent.worktree);
31737
31817
  }
31818
+ if (closed) return;
31738
31819
  await setAvailable(member.creds, true);
31739
31820
  markedAt.delete(name);
31740
31821
  } catch (err) {
31741
31822
  log(`not starting ${name}: ${err instanceof Error ? err.message : String(err)}`);
31742
31823
  continue;
31743
31824
  }
31825
+ if (closed) {
31826
+ await markStopped(member);
31827
+ return;
31828
+ }
31744
31829
  lastBusy.set(name, Date.now());
31745
31830
  sup.startAgent(name);
31746
31831
  log(`started ${name} (ready ${summary2.ready})`);
@@ -31766,9 +31851,25 @@ function startPoolLoop(run4, sup, record, account) {
31766
31851
  busy = false;
31767
31852
  }
31768
31853
  };
31769
- void tick();
31770
- const timer = setInterval(() => void tick(), reconcileMs);
31771
- return () => clearInterval(timer);
31854
+ let inflight = null;
31855
+ const schedule = () => {
31856
+ if (inflight) return;
31857
+ inflight = tick().finally(() => {
31858
+ inflight = null;
31859
+ });
31860
+ };
31861
+ schedule();
31862
+ const timer = setInterval(schedule, reconcileMs);
31863
+ const stop = (() => {
31864
+ closed = true;
31865
+ clearInterval(timer);
31866
+ });
31867
+ stop.markRunningStopped = async () => {
31868
+ closed = true;
31869
+ const running = members.filter((m) => sup.isActive(m.agent.name));
31870
+ await Promise.all([inflight, ...running.map((m) => markStopped(m))]);
31871
+ };
31872
+ return stop;
31772
31873
  }
31773
31874
  var MODEL_REFRESH_MS = 15e3;
31774
31875
  function desiredIdentity(api, root, nodeToken, log) {
@@ -31854,10 +31955,12 @@ async function nodeUpDesired() {
31854
31955
  sup.start();
31855
31956
  const identity = createIdentityRefresher(capacityDir());
31856
31957
  const loops = [];
31958
+ const poolLoops = [];
31857
31959
  const stopAll = [];
31858
31960
  let rebuild = null;
31859
31961
  const rebuildLoops = () => {
31860
31962
  for (const stop of loops.splice(0)) stop();
31963
+ poolLoops.splice(0);
31861
31964
  const runnable = allAgents(latest).filter((a) => sup.has(a.name));
31862
31965
  const credsOf = (a) => readCreds(a.name);
31863
31966
  const byRepo = /* @__PURE__ */ new Map();
@@ -31875,10 +31978,12 @@ async function nodeUpDesired() {
31875
31978
  });
31876
31979
  if (members.length === 0) continue;
31877
31980
  poolStates[pool.pool] = poolStates[pool.pool] ?? { members: members.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null };
31878
- loops.push(startPoolLoop({ pool, members, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
31981
+ const loop = startPoolLoop({ pool, members, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
31879
31982
  poolStates[name] = state;
31880
31983
  writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
31881
- }, () => identity.current()));
31984
+ }, () => identity.current());
31985
+ loops.push(loop);
31986
+ poolLoops.push(loop);
31882
31987
  }
31883
31988
  for (const name of Object.keys(poolStates)) if (!latest.pools.some((p) => p.pool === name)) delete poolStates[name];
31884
31989
  };
@@ -31957,10 +32062,7 @@ async function nodeUpDesired() {
31957
32062
  }, 6e4);
31958
32063
  const shutdown = async () => {
31959
32064
  clearInterval(keepAlive);
31960
- for (const stop of [...loops, ...stopAll, stopHeartbeat, janitor.stop]) stop();
31961
- console.log(source_default.dim("\n stopping agents\u2026"));
31962
- await sup.stop();
31963
- process.exit(0);
32065
+ await shutdownNode({ stops: [...loops, ...stopAll, stopHeartbeat, janitor.stop], sup, poolLoops: [...poolLoops], exit: (code) => process.exit(code) });
31964
32066
  };
31965
32067
  const janitor = startJanitorService({ roots: [root], managed: () => managedOfSpec(latest), nodeId: node2.nodeId, log: (line) => console.log(source_default.dim(` ${line}`)) });
31966
32068
  const stopHeartbeat = startNodeHeartbeat(
@@ -32094,14 +32196,12 @@ async function nodeUpCommand(specFile, opts = {}) {
32094
32196
  `));
32095
32197
  const stopLoops = [];
32096
32198
  const ownerBases = createOwnerPoolBases(ownerPoolBasesDepsFor(opts));
32199
+ const poolLoops = [];
32097
32200
  let stopping = false;
32098
32201
  const shutdown = async () => {
32099
32202
  if (stopping) return;
32100
32203
  stopping = true;
32101
- for (const stopLoop of stopLoops) stopLoop();
32102
- console.log(source_default.dim("\n stopping agents\u2026"));
32103
- await sup.stop();
32104
- process.exit(0);
32204
+ await shutdownNode({ stops: stopLoops, sup, poolLoops, exit: (code) => process.exit(code) });
32105
32205
  };
32106
32206
  process.on("SIGINT", shutdown);
32107
32207
  process.on("SIGTERM", shutdown);
@@ -32146,10 +32246,12 @@ async function nodeUpCommand(specFile, opts = {}) {
32146
32246
  continue;
32147
32247
  }
32148
32248
  poolStates[pool.pool] = { members: ready.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null, ...pool.ownerPool ? { ownerPool: true } : {} };
32149
- stopLoops.push(startPoolLoop({ pool, members: ready, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
32249
+ const loop = startPoolLoop({ pool, members: ready, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
32150
32250
  poolStates[name] = state;
32151
32251
  writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
32152
- }, () => identity.current()));
32252
+ }, () => identity.current());
32253
+ stopLoops.push(loop);
32254
+ poolLoops.push(loop);
32153
32255
  }
32154
32256
  } catch (err) {
32155
32257
  console.error(source_default.red(`
@@ -33964,7 +34066,7 @@ var token = program2.command("token").description("Manage your agent's API token
33964
34066
  token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
33965
34067
  token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
33966
34068
  token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
33967
- program2.command("onboard <github-url>").description("Set a GitHub repo up for Shipster: clone it, write a node spec (tier-2 architect, reviewer, writer pool) and print one device code that grants every agent. Starts nothing").option("--account <login>", "GitHub account to clone and work as, from `gh auth` (for example a personal account)").option("--dir <path>", "Parent directory for the clone, worktrees and spec (default ~/github)").option("--architect <runtime>", "Runtime for the tier-2 architect (asked for when there is a terminal and no flag)").option("--writer <runtime>", "Runtime for the writer pool (asked for when there is a terminal and no flag)").option("--reviewer <runtime>", "Runtime for the reviewer; may match the writers, with a warning (asked for when there is a terminal and no flag)").option("--pool-max <n>", "Writer pool size (default 2)").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
34069
+ program2.command("onboard <github-url>").description("Set a GitHub repo up for Shipster: clone it, write a node spec (tier-2 architect, reviewer, writer pool) and print one device code that grants every agent. Starts nothing").option("--account <login>", "GitHub account to clone and work as, from `gh auth` (for example a personal account)").option("--dir <path>", "Parent directory for the clone, worktrees and spec (default ~/github)").option("--architect <runtime>", "Runtime for the tier-2 architect (asked for when there is a terminal and no flag)").option("--writer <runtime>", "Runtime for the writer pool (asked for when there is a terminal and no flag)").option("--reviewer <runtime>", "Runtime for the reviewer; may match the writers, with a warning (asked for when there is a terminal and no flag)").option("--pool-max <n>", "Writer pool size (default 2)").option("--writer-models <list>", "One to three writer models, cheapest first, comma-separated: one writer pool per tier and a reviewer pool, all scaling to zero").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
33968
34070
  var node = program2.command("node").description("Run this machine's agents headless and keep them running");
33969
34071
  node.command("up [spec]").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them). With no spec file, follows the desired state the dashboard holds for this enrolled node").option(
33970
34072
  "--invite <name=code>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipster/cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [