@pitboss/cli 0.4.8 → 0.4.9

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/cli.js CHANGED
@@ -10864,7 +10864,7 @@ var require_lib2 = __commonJS({
10864
10864
  return matches;
10865
10865
  };
10866
10866
  exports.analyse = analyse;
10867
- var detectFile = (filepath, opts = {}) => new Promise((resolve8, reject) => {
10867
+ var detectFile = (filepath, opts = {}) => new Promise((resolve9, reject) => {
10868
10868
  let fd;
10869
10869
  const fs = (0, node_1.default)();
10870
10870
  const handler = (err, buffer) => {
@@ -10874,7 +10874,7 @@ var require_lib2 = __commonJS({
10874
10874
  if (err) {
10875
10875
  reject(err);
10876
10876
  } else if (buffer) {
10877
- resolve8((0, exports.detect)(buffer));
10877
+ resolve9((0, exports.detect)(buffer));
10878
10878
  } else {
10879
10879
  reject(new Error("No error and no buffer received"));
10880
10880
  }
@@ -17281,13 +17281,13 @@ var PromisePolyfill = class extends Promise {
17281
17281
  // Available starting from Node 22
17282
17282
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
17283
17283
  static withResolver() {
17284
- let resolve8;
17284
+ let resolve9;
17285
17285
  let reject;
17286
17286
  const promise = new Promise((res, rej) => {
17287
- resolve8 = res;
17287
+ resolve9 = res;
17288
17288
  reject = rej;
17289
17289
  });
17290
- return { promise, resolve: resolve8, reject };
17290
+ return { promise, resolve: resolve9, reject };
17291
17291
  }
17292
17292
  };
17293
17293
 
@@ -17324,7 +17324,7 @@ function createPrompt(view) {
17324
17324
  });
17325
17325
  output.mute();
17326
17326
  const screen = new ScreenManager(rl);
17327
- const { promise, resolve: resolve8, reject } = PromisePolyfill.withResolver();
17327
+ const { promise, resolve: resolve9, reject } = PromisePolyfill.withResolver();
17328
17328
  const cancel = () => reject(new CancelPromptError());
17329
17329
  if (signal) {
17330
17330
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -17355,7 +17355,7 @@ function createPrompt(view) {
17355
17355
  try {
17356
17356
  const nextView = view(config, (value) => {
17357
17357
  if (effectsSettled) {
17358
- resolve8(value);
17358
+ resolve9(value);
17359
17359
  } else {
17360
17360
  pendingDone = { value };
17361
17361
  }
@@ -17378,7 +17378,7 @@ function createPrompt(view) {
17378
17378
  if (pendingDone !== null) {
17379
17379
  const { value } = pendingDone;
17380
17380
  pendingDone = null;
17381
- resolve8(value);
17381
+ resolve9(value);
17382
17382
  }
17383
17383
  });
17384
17384
  };
@@ -17533,7 +17533,7 @@ var ExternalEditor = class {
17533
17533
  }
17534
17534
  runAsync(callback) {
17535
17535
  this.createTempFile();
17536
- const promise = new Promise((resolve8, reject) => {
17536
+ const promise = new Promise((resolve9, reject) => {
17537
17537
  try {
17538
17538
  const editorProcess = spawn(this.editor.bin, this.editorArgs(), {
17539
17539
  shell: false,
@@ -17544,7 +17544,7 @@ var ExternalEditor = class {
17544
17544
  });
17545
17545
  editorProcess.once("exit", (code) => {
17546
17546
  this.lastExitStatus = code ?? 0;
17547
- resolve8();
17547
+ resolve9();
17548
17548
  });
17549
17549
  } catch (launchError) {
17550
17550
  reject(new LaunchEditorError(launchError));
@@ -19107,6 +19107,7 @@ function isWorkerType(value) {
19107
19107
  return typeof value === "string" && WORKER_TYPES.includes(value);
19108
19108
  }
19109
19109
  var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
19110
+ var DEFAULT_REVIEW_SILENT_MS = 40 * 60 * 1e3;
19110
19111
  var POOL_STOPPED_REASON = "pool member stopped by pitboss node";
19111
19112
 
19112
19113
  // src/lib/runtimes.ts
@@ -20287,6 +20288,8 @@ function summarize(e, bodyChars = 80, agentId) {
20287
20288
  return `review ${p.review?.decision ?? "submitted"}: "${taskTitle}"`;
20288
20289
  case "task.review_overdue":
20289
20290
  return `review OVERDUE: "${taskTitle}"`;
20291
+ case "task.review_silent":
20292
+ return `review SILENT: "${taskTitle}"`;
20290
20293
  case "thread.created":
20291
20294
  return `new thread: "${threadTitle}"`;
20292
20295
  case "thread.concluded":
@@ -20324,6 +20327,7 @@ var KIND_STYLE = {
20324
20327
  "task.review_started": source_default.magenta,
20325
20328
  "task.review_submitted": source_default.magenta,
20326
20329
  "task.review_overdue": source_default.yellow,
20330
+ "task.review_silent": source_default.yellow,
20327
20331
  "thread.created": source_default.cyan,
20328
20332
  "thread.concluded": source_default.dim,
20329
20333
  "thread.message_ceiling": source_default.yellow
@@ -20599,14 +20603,13 @@ function hookCursorPromptCommand(opts) {
20599
20603
  process.stdout.write("{}");
20600
20604
  }
20601
20605
 
20602
- // src/commands/node.ts
20603
- import { execFileSync as execFileSync7, spawn as spawnChild } from "node:child_process";
20604
- import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync12, realpathSync as realpathSync5 } from "node:fs";
20605
- import { dirname as dirname11, resolve as resolve7 } from "node:path";
20606
- import { fileURLToPath as fileURLToPath3 } from "node:url";
20606
+ // src/commands/onboard.ts
20607
+ import { execFileSync as execFileSync7 } from "node:child_process";
20608
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync13, realpathSync as realpathSync5, writeFileSync as writeFileSync9 } from "node:fs";
20609
+ import { homedir as homedir6 } from "node:os";
20607
20610
 
20608
- // src/node/spec.ts
20609
- import { join as join15, resolve as resolve4, sep as sep2 } from "node:path";
20611
+ // src/node/onboard.ts
20612
+ import { join as join15, resolve as resolve4 } from "node:path";
20610
20613
 
20611
20614
  // ../claude-worker/src/session.ts
20612
20615
  import { spawn as spawn2 } from "child_process";
@@ -20667,7 +20670,8 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
20667
20670
  a one-line status on the task thread naming what you are checking) when you begin, then
20668
20671
  run the tests, compare the diff against the spec the task names, then
20669
20672
  \`mcp__relai__submit_review\` \u2014 \`approve\`, or \`reject\` with a specific note the author
20670
- can act on. Do this even when get_my_tasks is empty.
20673
+ can act on. When the task is a goal's build with a PR, pass \`headSha\`: the full sha of the
20674
+ commit you reviewed. Do this even when get_my_tasks is empty.
20671
20675
  If the change touches a long-running service or a third-party runtime integration (SSE/event streams, launchd/systemd, auth headers, a client library), run the changed path once before approving and quote what it printed. A type cast past a library's own types is a reason to stop and run it.
20672
20676
 
20673
20677
  4. Call \`mcp__relai__get_my_tasks\` \u2014 if it returns no tasks, stop immediately.
@@ -20909,7 +20913,11 @@ Find the default branch with \`git remote show origin\`.
20909
20913
  \`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
20910
20914
  the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
20911
20915
  \`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
20912
- Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
20916
+ Before writing any build task, choose the goal's reviewer (for a \`reviewer_agent\` gate): a
20917
+ \`reviewer\` agent, on a different \`workerType\` from the writers when one exists. Never assign that
20918
+ agent a build task, and never leave the only reviewers busy building: a goal whose every reviewer
20919
+ built part of it has nobody left to review it. The API defaults a build task with no
20920
+ \`specialization\` to \`writer\` when the repo has writers. Make them small
20913
20921
  and independently testable so several agents can work at once. Use \`blockedBy\` only where
20914
20922
  one task truly needs another's result. Each description is self-contained: what to change,
20915
20923
  which files, how to test it, and the acceptance criteria.
@@ -21170,6 +21178,18 @@ async function recordSessionUsage(conn, startIds, usage, logPrefix) {
21170
21178
  await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
21171
21179
  }
21172
21180
 
21181
+ // ../claude-worker/src/model.ts
21182
+ import { readFileSync as readFileSync11 } from "node:fs";
21183
+ function pinnedModel(env2 = process.env) {
21184
+ const file = env2.PITBOSS_MODEL_FILE;
21185
+ if (!file) return void 0;
21186
+ try {
21187
+ return readFileSync11(file, "utf-8").trim() || void 0;
21188
+ } catch {
21189
+ return void 0;
21190
+ }
21191
+ }
21192
+
21173
21193
  // ../claude-worker/src/session.ts
21174
21194
  function mcpServerEntry() {
21175
21195
  return fileURLToPath2(new URL("../../mcp-server/src/index.ts", import.meta.url));
@@ -21207,7 +21227,7 @@ async function runClaudeSession(config) {
21207
21227
  const mcpConfigPath = writeMcpConfig(config);
21208
21228
  const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
21209
21229
  try {
21210
- return await new Promise((resolve8, reject) => {
21230
+ return await new Promise((resolve9, reject) => {
21211
21231
  const args = [
21212
21232
  "--print",
21213
21233
  "--mcp-config",
@@ -21218,7 +21238,7 @@ async function runClaudeSession(config) {
21218
21238
  "stream-json",
21219
21239
  "--verbose",
21220
21240
  "--model",
21221
- config.model,
21241
+ pinnedModel() ?? config.model,
21222
21242
  "--add-dir",
21223
21243
  config.repoPath
21224
21244
  ];
@@ -21287,7 +21307,7 @@ async function runClaudeSession(config) {
21287
21307
  const detail = (resultError || stderrOutput || "").trim();
21288
21308
  reject(new Error(`claude exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21289
21309
  } else {
21290
- resolve8(unique);
21310
+ resolve9(unique);
21291
21311
  }
21292
21312
  };
21293
21313
  finish.then(settle, settle);
@@ -21339,8 +21359,8 @@ function classifySessionError(text) {
21339
21359
 
21340
21360
  // ../claude-worker/src/adapters/cursor.ts
21341
21361
  var bin = () => process.env.CURSOR_BIN ?? "cursor-agent";
21342
- function cursorArgs(repoPath, prompt) {
21343
- return ["-p", "--trust", "--force", "--output-format", "stream-json", "--workspace", repoPath, prompt];
21362
+ function cursorArgs(repoPath, prompt, model) {
21363
+ return ["-p", "--trust", "--force", "--output-format", "stream-json", "--workspace", repoPath, ...model ? ["--model", model] : [], prompt];
21344
21364
  }
21345
21365
  function readCursorEvent(line) {
21346
21366
  let event;
@@ -21364,10 +21384,10 @@ function readCursorEvent(line) {
21364
21384
  return {};
21365
21385
  }
21366
21386
  async function runCursorSession(config) {
21367
- const args = cursorArgs(config.repoPath, buildPrompt(config));
21387
+ const args = cursorArgs(config.repoPath, buildPrompt(config), pinnedModel());
21368
21388
  console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
21369
21389
  const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
21370
- return new Promise((resolve8, reject) => {
21390
+ return new Promise((resolve9, reject) => {
21371
21391
  const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21372
21392
  console.log(`[cursor-worker] Process PID: ${proc.pid}`);
21373
21393
  const toolsUsed = [];
@@ -21406,7 +21426,7 @@ async function runCursorSession(config) {
21406
21426
  const detail = (resultError || stderrOutput).trim();
21407
21427
  reject(new Error(`cursor-agent exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21408
21428
  } else {
21409
- resolve8([...new Set(toolsUsed)]);
21429
+ resolve9([...new Set(toolsUsed)]);
21410
21430
  }
21411
21431
  };
21412
21432
  finish.then(settle, settle);
@@ -21445,10 +21465,11 @@ import { unlinkSync as unlinkSync2 } from "fs";
21445
21465
  var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
21446
21466
  var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
21447
21467
  var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
21448
- function copilotArgs(repoPath, prompt, mcpConfigPath) {
21468
+ function copilotArgs(repoPath, prompt, mcpConfigPath, model) {
21449
21469
  return [
21450
21470
  "-p",
21451
21471
  prompt,
21472
+ ...model ? ["--model", model] : [],
21452
21473
  "--allow-all-tools",
21453
21474
  "--output-format",
21454
21475
  "json",
@@ -21478,9 +21499,9 @@ function readCopilotEvent(line) {
21478
21499
  }
21479
21500
  async function runCopilotSession(config) {
21480
21501
  const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
21481
- const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
21502
+ const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath, pinnedModel());
21482
21503
  console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
21483
- return new Promise((resolve8, reject) => {
21504
+ return new Promise((resolve9, reject) => {
21484
21505
  const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21485
21506
  console.log(`[copilot-worker] Process PID: ${proc.pid}`);
21486
21507
  const toolsUsed = [];
@@ -21517,7 +21538,7 @@ async function runCopilotSession(config) {
21517
21538
  const detail = (sessionError || stderrOutput).trim();
21518
21539
  reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21519
21540
  } else {
21520
- resolve8([...new Set(toolsUsed)]);
21541
+ resolve9([...new Set(toolsUsed)]);
21521
21542
  }
21522
21543
  });
21523
21544
  proc.on("error", (err) => {
@@ -21551,7 +21572,7 @@ var bin3 = () => process.env.CODEX_BIN ?? "codex";
21551
21572
  var codexHome = () => process.env.CODEX_HOME ?? join14(homedir5(), ".codex");
21552
21573
  var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
21553
21574
  var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
21554
- function codexArgs(repoPath, prompt, profile) {
21575
+ function codexArgs(repoPath, prompt, profile, model) {
21555
21576
  return [
21556
21577
  "exec",
21557
21578
  "--json",
@@ -21562,6 +21583,7 @@ function codexArgs(repoPath, prompt, profile) {
21562
21583
  repoPath,
21563
21584
  "-p",
21564
21585
  profile,
21586
+ ...model ? ["-m", model] : [],
21565
21587
  prompt
21566
21588
  ];
21567
21589
  }
@@ -21610,9 +21632,9 @@ async function runCodexSession(config) {
21610
21632
  } catch {
21611
21633
  }
21612
21634
  };
21613
- const args = codexArgs(config.repoPath, buildPrompt(config), profile);
21635
+ const args = codexArgs(config.repoPath, buildPrompt(config), profile, pinnedModel());
21614
21636
  console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
21615
- return new Promise((resolve8, reject) => {
21637
+ return new Promise((resolve9, reject) => {
21616
21638
  const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21617
21639
  console.log(`[codex-worker] Process PID: ${proc.pid}`);
21618
21640
  const toolsUsed = [];
@@ -21646,7 +21668,7 @@ async function runCodexSession(config) {
21646
21668
  const detail = (sessionError || stderrOutput).trim();
21647
21669
  reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21648
21670
  } else {
21649
- resolve8([...new Set(toolsUsed)]);
21671
+ resolve9([...new Set(toolsUsed)]);
21650
21672
  }
21651
21673
  });
21652
21674
  proc.on("error", (err) => {
@@ -21683,7 +21705,65 @@ var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
21683
21705
  // ../claude-worker/src/config.ts
21684
21706
  var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
21685
21707
 
21708
+ // src/node/onboard.ts
21709
+ var MAX_POOL = 30;
21710
+ var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
21711
+ function planOnboard(url, opts) {
21712
+ const match = GITHUB.exec(url.trim());
21713
+ if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
21714
+ const [, owner, name] = match;
21715
+ const writer = opts.writer ?? "claude";
21716
+ const reviewer = opts.reviewer ?? (writer === "claude" ? "copilot" : "claude");
21717
+ if (reviewer === writer) throw new Error(`the reviewer needs a different runtime from the writers (both are ${writer})`);
21718
+ for (const r of [writer, reviewer]) {
21719
+ if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
21720
+ }
21721
+ const poolMax = opts.poolMax ?? 2;
21722
+ if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
21723
+ throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
21724
+ }
21725
+ const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
21726
+ const parent = resolve4(opts.parent ?? join15(opts.home, "github"));
21727
+ const base2 = join15(parent, name);
21728
+ const nodeDir2 = join15(parent, `${name}-node`);
21729
+ const architect = join15(parent, `${name}-architect`);
21730
+ const review = join15(parent, `${name}-reviewer`);
21731
+ return {
21732
+ owner,
21733
+ name,
21734
+ cloneUrl: `https://github.com/${owner}/${name}.git`,
21735
+ base: base2,
21736
+ nodeDir: nodeDir2,
21737
+ specPath: join15(nodeDir2, "spec.json"),
21738
+ configDir: join15(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
21739
+ worktrees: [architect, review],
21740
+ spec: {
21741
+ api: "https://api.pitboss.dev",
21742
+ agents: [
21743
+ { name: `${id}-architect`, runtime: "claude", worktree: `../${name}-architect`, specialization: "architect", tier: 2 },
21744
+ { name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
21745
+ {
21746
+ pool: `${id}-pool`,
21747
+ runtime: writer,
21748
+ base: `../${name}`,
21749
+ worktreeRoot: `../${name}-pool`,
21750
+ specialization: "writer",
21751
+ min: 0,
21752
+ max: poolMax
21753
+ }
21754
+ ]
21755
+ }
21756
+ };
21757
+ }
21758
+ function originHost(origin) {
21759
+ const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
21760
+ if (url) return url[1].toLowerCase();
21761
+ const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
21762
+ return scp ? scp[1].toLowerCase() : null;
21763
+ }
21764
+
21686
21765
  // src/node/spec.ts
21766
+ import { join as join16, resolve as resolve5, sep as sep2 } from "node:path";
21687
21767
  var DEFAULT_API2 = "https://api.pitboss.dev";
21688
21768
  var MAX_POOL_SIZE = 32;
21689
21769
  var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
@@ -21695,7 +21775,14 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
21695
21775
  function poolMembers(pool) {
21696
21776
  return Array.from({ length: pool.max }, (_, i) => {
21697
21777
  const name = `${pool.pool}-${i + 1}`;
21698
- return { name, runtime: pool.runtime, worktree: join15(pool.worktreeRoot, name), specialization: pool.specialization };
21778
+ return {
21779
+ name,
21780
+ runtime: pool.runtime,
21781
+ worktree: join16(pool.worktreeRoot, name),
21782
+ specialization: pool.specialization,
21783
+ ...pool.tier ? { tier: pool.tier } : {},
21784
+ ...pool.model ? { model: pool.model } : {}
21785
+ };
21699
21786
  });
21700
21787
  }
21701
21788
  var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
@@ -21712,6 +21799,18 @@ function specializationOf(a, i) {
21712
21799
  }
21713
21800
  return specialization;
21714
21801
  }
21802
+ function tierOf(a, i, specialization) {
21803
+ if (a.tier === void 0) return specialization === "architect" ? { tier: 2 } : {};
21804
+ if (a.tier !== 1 && a.tier !== 2) throw new Error(`agents[${i}].tier must be 1 or 2`);
21805
+ return { tier: a.tier };
21806
+ }
21807
+ function modelOf(a, i) {
21808
+ if (a.model === void 0) return {};
21809
+ if (typeof a.model !== "string" || !/^[\w.:/@-]{1,100}$/.test(a.model)) {
21810
+ throw new Error(`agents[${i}].model must be a model name (letters, digits and . : / @ _ -), at most 100 characters`);
21811
+ }
21812
+ return { model: a.model };
21813
+ }
21715
21814
  function parsePool(a, i, specDir) {
21716
21815
  if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
21717
21816
  if (typeof a.pool !== "string" || !POOL_NAME.test(a.pool)) throw new Error(`agents[${i}].pool must be letters, digits, '.', '_' or '-' (at most 60)`);
@@ -21727,10 +21826,10 @@ function parsePool(a, i, specDir) {
21727
21826
  if (typeof min !== "number" || !Number.isInteger(min) || min < 0 || min > max) {
21728
21827
  throw new Error(`agents[${i}].min must be an integer from 0 to max`);
21729
21828
  }
21730
- const base2 = resolve4(specDir, a.base);
21731
- const worktreeRoot = resolve4(specDir, a.worktreeRoot);
21829
+ const base2 = resolve5(specDir, a.base);
21830
+ const worktreeRoot = resolve5(specDir, a.worktreeRoot);
21732
21831
  if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
21733
- return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, min, max };
21832
+ return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), min, max };
21734
21833
  }
21735
21834
  function parseSpec(text, specDir) {
21736
21835
  let raw;
@@ -21756,7 +21855,8 @@ function parseSpec(text, specDir) {
21756
21855
  if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
21757
21856
  if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
21758
21857
  const runtime = runtimeOf(a, i);
21759
- agents.push({ name: a.name, runtime, worktree: resolve4(specDir, a.worktree), specialization: specializationOf(a, i) });
21858
+ const specialization = specializationOf(a, i);
21859
+ agents.push({ name: a.name, runtime, worktree: resolve5(specDir, a.worktree), specialization, ...tierOf(a, i, specialization), ...modelOf(a, i) });
21760
21860
  });
21761
21861
  for (const p of pools) {
21762
21862
  for (const a of agents) {
@@ -21783,13 +21883,15 @@ function parseSpec(text, specDir) {
21783
21883
  }
21784
21884
 
21785
21885
  // src/node/store.ts
21786
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync11, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
21787
- import { dirname as dirname8, join as join16 } from "node:path";
21886
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync12, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
21887
+ import { dirname as dirname8, join as join17 } from "node:path";
21788
21888
  import { randomBytes as randomBytes4 } from "node:crypto";
21789
- var nodeDir = () => join16(configDir(), "node");
21790
- var credsPath = (name) => join16(nodeDir(), "agents", `${name}.json`);
21791
- var statePath2 = () => join16(nodeDir(), "state.json");
21792
- var logPath = (name) => join16(nodeDir(), "logs", `${name}.log`);
21889
+ var nodeDir = () => join17(configDir(), "node");
21890
+ var credsPath = (name) => join17(nodeDir(), "agents", `${name}.json`);
21891
+ var statePath2 = () => join17(nodeDir(), "state.json");
21892
+ var logPath = (name) => join17(nodeDir(), "logs", `${name}.log`);
21893
+ var activityPath = (name) => join17(nodeDir(), "activity", name);
21894
+ var modelPath = (name) => join17(nodeDir(), "model", name);
21793
21895
  function writePrivate(path3, value) {
21794
21896
  mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
21795
21897
  const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
@@ -21798,7 +21900,7 @@ function writePrivate(path3, value) {
21798
21900
  }
21799
21901
  function readJson(path3) {
21800
21902
  if (!existsSync8(path3)) return null;
21801
- return JSON.parse(readFileSync11(path3, "utf-8"));
21903
+ return JSON.parse(readFileSync12(path3, "utf-8"));
21802
21904
  }
21803
21905
  var readCreds = (name) => readJson(credsPath(name));
21804
21906
  var writeCreds = (name, creds) => writePrivate(credsPath(name), creds);
@@ -21807,14 +21909,187 @@ var removeCreds = (name) => {
21807
21909
  if (existsSync8(path3)) rmSync4(path3);
21808
21910
  };
21809
21911
  var listStoredAgentNames = () => {
21810
- const dir = join16(nodeDir(), "agents");
21912
+ const dir = join17(nodeDir(), "agents");
21811
21913
  if (!existsSync8(dir)) return [];
21812
21914
  return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
21813
21915
  };
21814
21916
  var readState = () => readJson(statePath2());
21815
21917
  var writeState = (state) => writePrivate(statePath2(), state);
21816
21918
 
21919
+ // src/node/pool.ts
21920
+ import { execFileSync as execFileSync6 } from "node:child_process";
21921
+ import { existsSync as existsSync9, lstatSync as lstatSync3, mkdirSync as mkdirSync8, realpathSync as realpathSync4 } from "node:fs";
21922
+ import { basename as basename2, dirname as dirname9, join as join18, resolve as resolve6 } from "node:path";
21923
+ var IDLE_STOP_MS = 10 * 6e4;
21924
+ var RECONCILE_MS = 15e3;
21925
+ var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
21926
+ var REMARK_AFTER_MS = 24 * 60 * 6e4;
21927
+ function positiveMs(raw, fallback2) {
21928
+ const n = Number(raw);
21929
+ return raw && Number.isInteger(n) && n > 0 ? n : fallback2;
21930
+ }
21931
+ function poolTimings(env2 = process.env) {
21932
+ return {
21933
+ reconcileMs: positiveMs(env2.PITBOSS_POOL_RECONCILE_MS, RECONCILE_MS),
21934
+ idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
21935
+ };
21936
+ }
21937
+ function effectiveBounds(spec, desired) {
21938
+ if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
21939
+ const max = Math.min(desired.max, spec.max);
21940
+ return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
21941
+ }
21942
+ function reconcilePool({ pending, members, min, max, idleStopMs }) {
21943
+ const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
21944
+ const running = members.filter((m) => m.running);
21945
+ const stopped = members.filter((m) => !m.running);
21946
+ let count = running.length;
21947
+ const start = [];
21948
+ const take = (m) => {
21949
+ start.push(m.name);
21950
+ count++;
21951
+ };
21952
+ for (const m of stopped) {
21953
+ if (count >= max) break;
21954
+ if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
21955
+ }
21956
+ const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
21957
+ let demand = pending - running.filter(isIdle).length;
21958
+ for (const m of spare()) {
21959
+ if (demand <= 0 || count >= max) break;
21960
+ take(m);
21961
+ demand--;
21962
+ }
21963
+ for (const m of spare()) {
21964
+ if (count >= min || count >= max) break;
21965
+ take(m);
21966
+ }
21967
+ if (start.length > 0) return { start, stop: [] };
21968
+ const stop = [];
21969
+ let surplus = running.filter(isIdle).length - pending;
21970
+ const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
21971
+ for (const m of eligible) {
21972
+ if (surplus <= 0 || count <= min) break;
21973
+ stop.push(m.name);
21974
+ count--;
21975
+ surplus--;
21976
+ }
21977
+ return { start, stop };
21978
+ }
21979
+ function summarizeTasks(ready, held, members) {
21980
+ const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
21981
+ const counts = Object.fromEntries(
21982
+ members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
21983
+ );
21984
+ const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
21985
+ let readyAssigned = 0;
21986
+ for (const t of ready) {
21987
+ const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
21988
+ if (!name) continue;
21989
+ counts[name].readyAssigned++;
21990
+ readyAssigned++;
21991
+ }
21992
+ for (const t of held) {
21993
+ const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
21994
+ if (!name) continue;
21995
+ if (t.status === "assigned") counts[name].assigned++;
21996
+ if (t.status === "in_progress") counts[name].inProgress++;
21997
+ }
21998
+ return { pending, ready: pending + readyAssigned, members: counts };
21999
+ }
22000
+ function poolRootProblem(root, protectedPaths) {
22001
+ for (const p of protectedPaths) {
22002
+ if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
22003
+ }
22004
+ return null;
22005
+ }
22006
+ function memberPathProblem(s) {
22007
+ if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
22008
+ if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
22009
+ if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
22010
+ return null;
22011
+ }
22012
+ function realpathAllowMissing(path3) {
22013
+ const abs = resolve6(path3);
22014
+ if (existsSync9(abs)) return realpathSync4.native(abs);
22015
+ return join18(realpathAllowMissing(dirname9(abs)), basename2(abs));
22016
+ }
22017
+ function run2(cwd, args) {
22018
+ try {
22019
+ return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
22020
+ } catch (err) {
22021
+ const stderr = err.stderr?.toString().trim();
22022
+ throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
22023
+ }
22024
+ }
22025
+ var commonDir = (cwd) => {
22026
+ const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
22027
+ return dir ? realpathSync4.native(dir) : null;
22028
+ };
22029
+ function defaultBranch(cwd) {
22030
+ const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
22031
+ let ref = head();
22032
+ if (!ref) {
22033
+ git(["remote", "set-head", "origin", "--auto"], cwd);
22034
+ ref = head();
22035
+ }
22036
+ if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
22037
+ return ref.slice("origin/".length);
22038
+ }
22039
+ function ensurePoolWorktrees(pool, otherWorktrees = []) {
22040
+ if (!existsSync9(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
22041
+ const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
22042
+ const baseCommon = commonDir(pool.base);
22043
+ if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
22044
+ const root = realpathAllowMissing(pool.worktreeRoot);
22045
+ const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
22046
+ if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
22047
+ const members = poolMembers({ ...pool, worktreeRoot: root });
22048
+ for (const m of members) {
22049
+ let symlink = false;
22050
+ try {
22051
+ symlink = lstatSync3(m.worktree).isSymbolicLink();
22052
+ } catch {
22053
+ symlink = false;
22054
+ }
22055
+ const exists = !symlink && existsSync9(m.worktree);
22056
+ const problem = memberPathProblem({
22057
+ path: m.worktree,
22058
+ symlink,
22059
+ exists,
22060
+ baseCommonDir: baseCommon,
22061
+ commonDir: exists ? commonDir(m.worktree) : null,
22062
+ tracked: !exists && !symlink && isTracked(m.worktree)
22063
+ });
22064
+ if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
22065
+ }
22066
+ const missing = members.filter((m) => !existsSync9(m.worktree));
22067
+ if (missing.length > 0) {
22068
+ mkdirSync8(root, { recursive: true });
22069
+ run2(pool.base, ["fetch", "-q", "origin"]);
22070
+ const branch = defaultBranch(pool.base);
22071
+ run2(pool.base, ["worktree", "prune"]);
22072
+ for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
22073
+ }
22074
+ return members.map((m) => realpathSync4.native(m.worktree));
22075
+ }
22076
+ function resetWorktree(worktree) {
22077
+ run2(worktree, ["fetch", "-q", "--prune", "origin"]);
22078
+ const target = `origin/${defaultBranch(worktree)}`;
22079
+ run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
22080
+ run2(worktree, ["reset", "-q", "--hard", target]);
22081
+ run2(worktree, ["clean", "-fdq"]);
22082
+ }
22083
+
21817
22084
  // src/node/identity.ts
22085
+ function requestedAgents(agents) {
22086
+ return agents.map((a) => ({
22087
+ name: a.name,
22088
+ workerType: getAdapter(a.runtime).workerType,
22089
+ specialization: a.specialization,
22090
+ ...a.tier ? { tier: a.tier } : {}
22091
+ }));
22092
+ }
21818
22093
  function collectInvites(agentNames, flags, env2) {
21819
22094
  const byLower = new Map(agentNames.map((n) => [n.toLowerCase(), n]));
21820
22095
  const out = /* @__PURE__ */ new Map();
@@ -21851,6 +22126,7 @@ function matchInvites(agents, invites) {
21851
22126
  }
21852
22127
  return { matched, ungranted, extra: left, refused };
21853
22128
  }
22129
+ var MAX_REQUESTED = 32;
21854
22130
  function groupByRepo(agents, originOf2) {
21855
22131
  const groups = /* @__PURE__ */ new Map();
21856
22132
  for (const agent of agents) {
@@ -21858,7 +22134,10 @@ function groupByRepo(agents, originOf2) {
21858
22134
  if (!remote) throw new Error(`${agent.name}: ${agent.worktree} has no git origin; each worktree must be a clone of the agent's repo`);
21859
22135
  groups.set(remote, [...groups.get(remote) ?? [], agent]);
21860
22136
  }
21861
- return [...groups].map(([remote, members]) => ({ remote, agents: members }));
22137
+ return [...groups].flatMap(([remote, members]) => Array.from({ length: Math.ceil(members.length / MAX_REQUESTED) }, (_, i) => ({
22138
+ remote,
22139
+ agents: members.slice(i * MAX_REQUESTED, (i + 1) * MAX_REQUESTED)
22140
+ })));
21862
22141
  }
21863
22142
  var originOf = (worktree) => git(["remote", "get-url", "origin"], worktree);
21864
22143
  async function abandonMinted(spec, agent, minted, reason) {
@@ -21964,10 +22243,12 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
21964
22243
  console.log(source_default.bold(`
21965
22244
  Approve ${repoName}`));
21966
22245
  console.log(` On the approval screen add these agents as workers:`);
21967
- for (const a of group.agents) console.log(` ${source_default.cyan(a.name)} ${source_default.dim(`${a.runtime}, ${a.specialization}`)}`);
22246
+ for (const a of group.agents) {
22247
+ console.log(` ${source_default.cyan(a.name)} ${source_default.dim(`${a.runtime}, ${a.specialization}${a.tier ? `, tier ${a.tier}` : ""}`)}`);
22248
+ }
21968
22249
  let grant;
21969
22250
  try {
21970
- grant = await requestDeviceGrant(spec.api, { repoName, remote: group.remote }, "pitboss node up");
22251
+ grant = await requestDeviceGrant(spec.api, { repoName, remote: group.remote, agents: requestedAgents(group.agents) }, "pitboss node up");
21971
22252
  } catch (err) {
21972
22253
  console.log(source_default.red(` \u2715 ${repoName}: ${err instanceof Error ? err.message : String(err)} ${group.agents.map((a) => a.name).join(", ")} will not start.`));
21973
22254
  continue;
@@ -22007,6 +22288,106 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
22007
22288
  return have;
22008
22289
  }
22009
22290
 
22291
+ // src/commands/onboard.ts
22292
+ function isGitHubHost(host) {
22293
+ if (!host) return false;
22294
+ if (host === "github.com") return true;
22295
+ try {
22296
+ const resolved = execFileSync7("ssh", ["-G", host], { encoding: "utf-8" }).match(/^hostname (\S+)$/m)?.[1];
22297
+ return resolved?.toLowerCase() === "github.com";
22298
+ } catch {
22299
+ return false;
22300
+ }
22301
+ }
22302
+ var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
22303
+ async function onboardCommand(url, opts) {
22304
+ try {
22305
+ const plan = planOnboard(url, {
22306
+ home: homedir6(),
22307
+ parent: opts.dir,
22308
+ writer: opts.writer,
22309
+ reviewer: opts.reviewer,
22310
+ poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax)
22311
+ });
22312
+ const env2 = { ...process.env };
22313
+ if (opts.account) {
22314
+ env2.GH_TOKEN = execFileSync7("gh", ["auth", "token", "--hostname", "github.com", "--user", opts.account], { encoding: "utf-8" }).trim();
22315
+ }
22316
+ if (existsSync10(plan.base)) {
22317
+ const origin = execFileSync7("git", ["-C", plan.base, "remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
22318
+ if (normalizeRepoUrl(origin) !== normalizeRepoUrl(plan.cloneUrl) || !isGitHubHost(originHost(origin))) {
22319
+ throw new Error(`${plan.base} exists but its origin is ${origin}, not ${plan.cloneUrl} on github.com`);
22320
+ }
22321
+ console.log(source_default.dim(` using the clone at ${plan.base}`));
22322
+ } else {
22323
+ execFileSync7("gh", ["repo", "clone", `${plan.owner}/${plan.name}`, plan.base], { stdio: "inherit", env: env2 });
22324
+ }
22325
+ const commonDir2 = (dir) => realpathSync5.native(
22326
+ execFileSync7("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf-8" }).trim()
22327
+ );
22328
+ for (const path3 of plan.worktrees) {
22329
+ if (!existsSync10(path3)) {
22330
+ execFileSync7("git", ["-C", plan.base, "worktree", "add", "--detach", path3], { stdio: "inherit" });
22331
+ } else if (commonDir2(path3) !== commonDir2(plan.base)) {
22332
+ throw new Error(`${path3} exists but is not a worktree of ${plan.base}; move it aside or pass --dir`);
22333
+ }
22334
+ }
22335
+ const specText = JSON.stringify(plan.spec, null, 2) + "\n";
22336
+ const parsed = parseSpec(specText, plan.nodeDir);
22337
+ if (existsSync10(plan.specPath) && readFileSync13(plan.specPath, "utf-8") !== specText && !opts.force) {
22338
+ throw new Error(`${plan.specPath} already exists with different contents; pass --force to replace it`);
22339
+ }
22340
+ mkdirSync9(plan.nodeDir, { recursive: true });
22341
+ writeFileSync9(plan.specPath, specText);
22342
+ process.env.PITBOSS_CONFIG_DIR = plan.configDir;
22343
+ mkdirSync9(plan.configDir, { recursive: true, mode: 448 });
22344
+ const specReal = realpathSync5.native(plan.specPath);
22345
+ const foreign = allAgents(parsed).map((a) => readCreds(a.name)).find((c) => c?.spec && c.spec !== specReal);
22346
+ if (foreign) throw new Error(`${plan.configDir} holds credentials for another node spec (${foreign.spec}); onboard will not overwrite them`);
22347
+ const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync5.native(a.worktree) }));
22348
+ const ownToken = process.env.GH_TOKEN;
22349
+ if (env2.GH_TOKEN) process.env.GH_TOKEN = env2.GH_TOKEN;
22350
+ let pooled;
22351
+ try {
22352
+ pooled = parsed.pools.flatMap((pool) => {
22353
+ const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
22354
+ return poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] }));
22355
+ });
22356
+ } finally {
22357
+ if (ownToken === void 0) delete process.env.GH_TOKEN;
22358
+ else process.env.GH_TOKEN = ownToken;
22359
+ }
22360
+ const everyone = [...plain, ...pooled];
22361
+ const seen = /* @__PURE__ */ new Map();
22362
+ for (const a of everyone) {
22363
+ const other = seen.get(a.worktree);
22364
+ if (other) throw new Error(`agents "${other}" and "${a.name}" resolve to the same worktree ${a.worktree}; each needs its own`);
22365
+ seen.set(a.worktree, a.name);
22366
+ }
22367
+ const creds = await ensureIdentities({ ...parsed, agents: everyone }, /* @__PURE__ */ new Map(), specReal);
22368
+ const missing = everyone.filter((a) => !creds.has(a.name)).map((a) => a.name);
22369
+ if (missing.length === everyone.length) throw new Error("no agent was granted; approve the device code again by re-running onboard");
22370
+ if (missing.length > 0) process.exitCode = 1;
22371
+ console.log(source_default.bold(`
22372
+ ${plan.name} is onboarded${missing.length ? source_default.yellow(` except ${missing.join(", ")}`) : ""}.`));
22373
+ console.log(` spec ${plan.specPath}`);
22374
+ console.log(` config ${plan.configDir}`);
22375
+ console.log(`
22376
+ Start it when you want it running (long-running, foreground):`);
22377
+ console.log(source_default.cyan(` cd ${shellQuote(plan.nodeDir)} && PITBOSS_CONFIG_DIR=${shellQuote(plan.configDir)} pitboss node up spec.json`));
22378
+ if (opts.account) console.log(source_default.dim(` with GH_TOKEN from \`gh auth token --user ${opts.account}\` in that shell`));
22379
+ } catch (err) {
22380
+ console.error(source_default.red(` \u2715 ${err instanceof Error ? err.message : String(err)}`));
22381
+ process.exitCode = 1;
22382
+ }
22383
+ }
22384
+
22385
+ // src/commands/node.ts
22386
+ import { execFileSync as execFileSync8, spawn as spawnChild } from "node:child_process";
22387
+ import { closeSync, existsSync as existsSync12, mkdirSync as mkdirSync11, openSync, readFileSync as readFileSync15, realpathSync as realpathSync6, rmSync as rmSync6 } from "node:fs";
22388
+ import { dirname as dirname12, resolve as resolve8 } from "node:path";
22389
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
22390
+
22010
22391
  // src/node/supervisor.ts
22011
22392
  function nextDelay(consecutiveFailures, { baseMs, maxMs }) {
22012
22393
  return Math.min(baseMs * 2 ** Math.max(0, consecutiveFailures - 1), maxMs);
@@ -22131,44 +22512,67 @@ var Supervisor = class {
22131
22512
  }
22132
22513
  };
22133
22514
 
22515
+ // src/node/model.ts
22516
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync10 } from "node:fs";
22517
+ import { dirname as dirname10 } from "node:path";
22518
+ function writeModelFiles(agents) {
22519
+ for (const a of agents) {
22520
+ const path3 = modelPath(a.name);
22521
+ if (!a.model) {
22522
+ rmSync5(path3, { force: true });
22523
+ continue;
22524
+ }
22525
+ let current = null;
22526
+ try {
22527
+ current = readFileSync14(path3, "utf-8");
22528
+ } catch {
22529
+ }
22530
+ if (current === a.model) continue;
22531
+ mkdirSync10(dirname10(path3), { recursive: true });
22532
+ const tmp = `${path3}.${process.pid}.tmp`;
22533
+ writeFileSync10(tmp, a.model);
22534
+ renameSync5(tmp, path3);
22535
+ }
22536
+ }
22537
+
22134
22538
  // src/node/entries.ts
22135
- import { existsSync as existsSync9 } from "node:fs";
22136
- import { dirname as dirname9, join as join17, resolve as resolve5 } from "node:path";
22539
+ import { existsSync as existsSync11 } from "node:fs";
22540
+ import { dirname as dirname11, join as join19, resolve as resolve7 } from "node:path";
22137
22541
  var MISSING_WORKER = "event-worker not found next to this CLI. Run node from a pitboss checkout, or set PITBOSS_EVENT_WORKER to packages/event-worker.";
22138
22542
  function checkoutWorker(dir) {
22139
22543
  return {
22140
- command: join17(dir, "node_modules", ".bin", "tsx"),
22141
- args: [join17(dir, "src", "index.ts")]
22544
+ command: join19(dir, "node_modules", ".bin", "tsx"),
22545
+ args: [join19(dir, "src", "index.ts")]
22142
22546
  };
22143
22547
  }
22144
22548
  function checkoutMcp(dir, execPath) {
22145
22549
  return {
22146
22550
  command: execPath,
22147
22551
  args: [
22148
- join17(dir, "node_modules", "tsx", "dist", "cli.mjs"),
22149
- join17(dir, "src", "index.ts")
22552
+ join19(dir, "node_modules", "tsx", "dist", "cli.mjs"),
22553
+ join19(dir, "src", "index.ts")
22150
22554
  ]
22151
22555
  };
22152
22556
  }
22153
22557
  function walkFor(here, packageName, exists) {
22154
- for (let dir = dirname9(here); dirname9(dir) !== dir; dir = dirname9(dir)) {
22155
- const candidate = join17(dir, packageName);
22156
- if (exists(join17(candidate, "src", "index.ts"))) return candidate;
22558
+ for (let dir = dirname11(here); dirname11(dir) !== dir; dir = dirname11(dir)) {
22559
+ const candidate = join19(dir, packageName);
22560
+ if (exists(join19(candidate, "src", "index.ts"))) return candidate;
22157
22561
  }
22158
22562
  return null;
22159
22563
  }
22160
22564
  function resolveEventWorker(opts) {
22161
22565
  const env2 = opts.env ?? process.env;
22162
22566
  const execPath = opts.execPath ?? process.execPath;
22163
- const exists = opts.exists ?? existsSync9;
22567
+ const exists = opts.exists ?? existsSync11;
22164
22568
  const override = env2.PITBOSS_EVENT_WORKER;
22165
22569
  if (override) {
22166
- const resolved = resolve5(override);
22570
+ const resolved = resolve7(override);
22167
22571
  if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
22168
- if (exists(join17(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
22572
+ if (exists(join19(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
22169
22573
  throw new Error(MISSING_WORKER);
22170
22574
  }
22171
- const bundled = join17(dirname9(opts.here), "event-worker.js");
22575
+ const bundled = join19(dirname11(opts.here), "event-worker.js");
22172
22576
  if (exists(bundled)) return { command: execPath, args: [bundled] };
22173
22577
  const dir = walkFor(opts.here, "event-worker", exists);
22174
22578
  if (dir) return checkoutWorker(dir);
@@ -22176,8 +22580,8 @@ function resolveEventWorker(opts) {
22176
22580
  }
22177
22581
  function resolveMcpServer(opts) {
22178
22582
  const execPath = opts.execPath ?? process.execPath;
22179
- const exists = opts.exists ?? existsSync9;
22180
- const bundled = join17(dirname9(opts.here), "mcp.js");
22583
+ const exists = opts.exists ?? existsSync11;
22584
+ const bundled = join19(dirname11(opts.here), "mcp.js");
22181
22585
  if (exists(bundled)) return { command: execPath, args: [bundled] };
22182
22586
  const dir = walkFor(opts.here, "mcp-server", exists);
22183
22587
  if (dir) return checkoutMcp(dir, execPath);
@@ -22285,176 +22689,41 @@ function loadPruneTargets(names) {
22285
22689
  });
22286
22690
  }
22287
22691
 
22288
- // src/node/pool.ts
22289
- import { execFileSync as execFileSync6 } from "node:child_process";
22290
- import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as mkdirSync8, realpathSync as realpathSync4 } from "node:fs";
22291
- import { basename as basename2, dirname as dirname10, join as join18, resolve as resolve6 } from "node:path";
22292
- var IDLE_STOP_MS = 10 * 6e4;
22293
- var RECONCILE_MS = 15e3;
22294
- var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
22295
- var REMARK_AFTER_MS = 24 * 60 * 6e4;
22296
- function positiveMs(raw, fallback2) {
22297
- const n = Number(raw);
22298
- return raw && Number.isInteger(n) && n > 0 ? n : fallback2;
22299
- }
22300
- function poolTimings(env2 = process.env) {
22301
- return {
22302
- reconcileMs: positiveMs(env2.PITBOSS_POOL_RECONCILE_MS, RECONCILE_MS),
22303
- idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
22304
- };
22305
- }
22306
- function effectiveBounds(spec, desired) {
22307
- if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
22308
- const max = Math.min(desired.max, spec.max);
22309
- return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
22310
- }
22311
- function reconcilePool({ pending, members, min, max, idleStopMs }) {
22312
- const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
22313
- const running = members.filter((m) => m.running);
22314
- const stopped = members.filter((m) => !m.running);
22315
- let count = running.length;
22316
- const start = [];
22317
- const take = (m) => {
22318
- start.push(m.name);
22319
- count++;
22320
- };
22321
- for (const m of stopped) {
22322
- if (count >= max) break;
22323
- if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
22324
- }
22325
- const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
22326
- let demand = pending - running.filter(isIdle).length;
22327
- for (const m of spare()) {
22328
- if (demand <= 0 || count >= max) break;
22329
- take(m);
22330
- demand--;
22331
- }
22332
- for (const m of spare()) {
22333
- if (count >= min || count >= max) break;
22334
- take(m);
22335
- }
22336
- if (start.length > 0) return { start, stop: [] };
22337
- const stop = [];
22338
- let surplus = running.filter(isIdle).length - pending;
22339
- const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
22340
- for (const m of eligible) {
22341
- if (surplus <= 0 || count <= min) break;
22342
- stop.push(m.name);
22343
- count--;
22344
- surplus--;
22345
- }
22346
- return { start, stop };
22347
- }
22348
- function summarizeTasks(ready, held, members) {
22349
- const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
22350
- const counts = Object.fromEntries(
22351
- members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
22352
- );
22353
- const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
22354
- let readyAssigned = 0;
22355
- for (const t of ready) {
22356
- const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
22357
- if (!name) continue;
22358
- counts[name].readyAssigned++;
22359
- readyAssigned++;
22360
- }
22361
- for (const t of held) {
22362
- const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
22363
- if (!name) continue;
22364
- if (t.status === "assigned") counts[name].assigned++;
22365
- if (t.status === "in_progress") counts[name].inProgress++;
22366
- }
22367
- return { pending, ready: pending + readyAssigned, members: counts };
22368
- }
22369
- function poolRootProblem(root, protectedPaths) {
22370
- for (const p of protectedPaths) {
22371
- if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
22372
- }
22373
- return null;
22374
- }
22375
- function memberPathProblem(s) {
22376
- if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
22377
- if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
22378
- if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
22379
- return null;
22380
- }
22381
- function realpathAllowMissing(path3) {
22382
- const abs = resolve6(path3);
22383
- if (existsSync10(abs)) return realpathSync4.native(abs);
22384
- return join18(realpathAllowMissing(dirname10(abs)), basename2(abs));
22385
- }
22386
- function run2(cwd, args) {
22387
- try {
22388
- return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
22389
- } catch (err) {
22390
- const stderr = err.stderr?.toString().trim();
22391
- throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
22392
- }
22393
- }
22394
- var commonDir = (cwd) => {
22395
- const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
22396
- return dir ? realpathSync4.native(dir) : null;
22397
- };
22398
- function defaultBranch(cwd) {
22399
- const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
22400
- let ref = head();
22401
- if (!ref) {
22402
- git(["remote", "set-head", "origin", "--auto"], cwd);
22403
- ref = head();
22404
- }
22405
- if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
22406
- return ref.slice("origin/".length);
22407
- }
22408
- function ensurePoolWorktrees(pool, otherWorktrees = []) {
22409
- if (!existsSync10(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
22410
- const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
22411
- const baseCommon = commonDir(pool.base);
22412
- if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
22413
- const root = realpathAllowMissing(pool.worktreeRoot);
22414
- const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
22415
- if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
22416
- const members = poolMembers({ ...pool, worktreeRoot: root });
22417
- for (const m of members) {
22418
- let symlink = false;
22419
- try {
22420
- symlink = lstatSync3(m.worktree).isSymbolicLink();
22421
- } catch {
22422
- symlink = false;
22423
- }
22424
- const exists = !symlink && existsSync10(m.worktree);
22425
- const problem = memberPathProblem({
22426
- path: m.worktree,
22427
- symlink,
22428
- exists,
22429
- baseCommonDir: baseCommon,
22430
- commonDir: exists ? commonDir(m.worktree) : null,
22431
- tracked: !exists && !symlink && isTracked(m.worktree)
22432
- });
22433
- if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
22434
- }
22435
- const missing = members.filter((m) => !existsSync10(m.worktree));
22436
- if (missing.length > 0) {
22437
- mkdirSync8(root, { recursive: true });
22438
- run2(pool.base, ["fetch", "-q", "origin"]);
22439
- const branch = defaultBranch(pool.base);
22440
- run2(pool.base, ["worktree", "prune"]);
22441
- for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
22442
- }
22443
- return members.map((m) => realpathSync4.native(m.worktree));
22444
- }
22445
- function resetWorktree(worktree) {
22446
- run2(worktree, ["fetch", "-q", "--prune", "origin"]);
22447
- const target = `origin/${defaultBranch(worktree)}`;
22448
- run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
22449
- run2(worktree, ["reset", "-q", "--hard", target]);
22450
- run2(worktree, ["clean", "-fdq"]);
22692
+ // src/node/activity.ts
22693
+ var ACTIVITY_REPORT_MS = 15e3;
22694
+ var REVIEW_HEARTBEAT_MS = 6e4;
22695
+ function deriveActivity({ running, sessionActive, reviewing, waiting }) {
22696
+ if (!running) return "stopped";
22697
+ if (sessionActive) return "working";
22698
+ if (reviewing) return "reviewing";
22699
+ return waiting ? "waiting" : "idle";
22700
+ }
22701
+ function heldAgentIds(tasks) {
22702
+ const reviewing = /* @__PURE__ */ new Set();
22703
+ const waiting = /* @__PURE__ */ new Set();
22704
+ for (const t of tasks) {
22705
+ if (t.assignedTo && (t.status === "blocked" || t.status === "pending_verification")) waiting.add(t.assignedTo);
22706
+ if (t.status === "pending_verification" && t.verifyReviewerId && t.metadata?.reviewStartedBy === t.verifyReviewerId) reviewing.add(t.verifyReviewerId);
22707
+ }
22708
+ return { reviewing, waiting };
22709
+ }
22710
+ function startedReviewTaskIds(tasks) {
22711
+ const byAgent = /* @__PURE__ */ new Map();
22712
+ for (const t of tasks) {
22713
+ if (t.status !== "pending_verification" || !t.verifyReviewerId || t.metadata?.reviewStartedBy !== t.verifyReviewerId) continue;
22714
+ byAgent.set(t.verifyReviewerId, [...byAgent.get(t.verifyReviewerId) ?? [], t.id]);
22715
+ }
22716
+ return byAgent;
22717
+ }
22718
+ function reviewerIsLive({ running, sessionActive }) {
22719
+ return running && sessionActive;
22451
22720
  }
22452
22721
 
22453
22722
  // src/commands/node.ts
22454
22723
  var HERE = fileURLToPath3(import.meta.url);
22455
22724
  function inspectProcess(pid) {
22456
22725
  try {
22457
- const out = execFileSync7("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
22726
+ const out = execFileSync8("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
22458
22727
  const match = /^(\d+)\s+(.*)$/.exec(out);
22459
22728
  return match ? { pgid: Number(match[1]), command: match[2] } : null;
22460
22729
  } catch {
@@ -22478,7 +22747,9 @@ function childSpec(agent, creds, worker, onDemand = false) {
22478
22747
  REPO_ID: creds.repoId,
22479
22748
  REPO_PATH: agent.worktree,
22480
22749
  SPECIALIZATION: agent.specialization,
22481
- WORKER_RUNTIME: agent.runtime
22750
+ WORKER_RUNTIME: agent.runtime,
22751
+ PITBOSS_ACTIVITY_FILE: activityPath(agent.name),
22752
+ PITBOSS_MODEL_FILE: resolve8(modelPath(agent.name))
22482
22753
  }
22483
22754
  };
22484
22755
  }
@@ -22494,12 +22765,13 @@ function prepareAgent(agent, creds, mcp) {
22494
22765
  excludeIfUntracked(root, target);
22495
22766
  },
22496
22767
  run(bin4, args, cwd) {
22497
- execFileSync7(bin4, args, { cwd, stdio: "ignore" });
22768
+ execFileSync8(bin4, args, { cwd, stdio: "ignore" });
22498
22769
  }
22499
22770
  });
22500
22771
  }
22501
22772
  function spawnWorker(spec) {
22502
- mkdirSync9(dirname11(spec.logPath), { recursive: true, mode: 448 });
22773
+ mkdirSync11(dirname12(spec.logPath), { recursive: true, mode: 448 });
22774
+ rmSync6(activityPath(spec.name), { force: true });
22503
22775
  const log = openSync(spec.logPath, "a", 384);
22504
22776
  try {
22505
22777
  const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
@@ -22546,6 +22818,63 @@ async function reportScale(creds, event) {
22546
22818
  });
22547
22819
  if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
22548
22820
  }
22821
+ async function reportActivity(creds, state) {
22822
+ const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/activity`, {
22823
+ method: "PUT",
22824
+ headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
22825
+ body: JSON.stringify({ state })
22826
+ });
22827
+ if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/activity returned HTTP ${res.status}`);
22828
+ }
22829
+ function startActivityLoop(agents, sup) {
22830
+ let busy = false;
22831
+ const sent = /* @__PURE__ */ new Map();
22832
+ const beatAt = /* @__PURE__ */ new Map();
22833
+ const heartbeatReviews = async (creds, name, live, taskIds) => {
22834
+ if (!live || taskIds.length === 0 || Date.now() - (beatAt.get(name) ?? 0) < REVIEW_HEARTBEAT_MS) return;
22835
+ beatAt.set(name, Date.now());
22836
+ for (const id of taskIds) {
22837
+ const res = await fetch(`${creds.api}/tasks/${encodeURIComponent(id)}/review/start`, {
22838
+ method: "POST",
22839
+ headers: { Authorization: `Bearer ${creds.token}` }
22840
+ });
22841
+ if (!res.ok) throw new Error(`POST /tasks/${id}/review/start returned HTTP ${res.status}`);
22842
+ }
22843
+ };
22844
+ const tick = async () => {
22845
+ if (busy) return;
22846
+ busy = true;
22847
+ try {
22848
+ const reader = agents[0].creds;
22849
+ const held = await fetchTasks(reader, "status=blocked,pending_verification");
22850
+ const { reviewing, waiting } = heldAgentIds(held);
22851
+ const reviewIds = startedReviewTaskIds(held);
22852
+ for (const { name, creds } of agents) {
22853
+ const signals2 = {
22854
+ running: sup.isActive(name),
22855
+ sessionActive: existsSync12(activityPath(name))
22856
+ };
22857
+ const live = reviewerIsLive(signals2);
22858
+ const state = deriveActivity({
22859
+ ...signals2,
22860
+ reviewing: reviewing.has(creds.agentId),
22861
+ waiting: waiting.has(creds.agentId)
22862
+ });
22863
+ await heartbeatReviews(creds, name, live, reviewIds.get(creds.agentId) ?? []);
22864
+ if (sent.get(name) === state) continue;
22865
+ await reportActivity(creds, state);
22866
+ sent.set(name, state);
22867
+ }
22868
+ } catch (err) {
22869
+ console.log(`${(/* @__PURE__ */ new Date()).toISOString()} activity: ${err instanceof Error ? err.message : String(err)}`);
22870
+ } finally {
22871
+ busy = false;
22872
+ }
22873
+ };
22874
+ void tick();
22875
+ const timer = setInterval(() => void tick(), ACTIVITY_REPORT_MS);
22876
+ return () => clearInterval(timer);
22877
+ }
22549
22878
  function startPoolLoop(run3, sup, record) {
22550
22879
  const { pool, members } = run3;
22551
22880
  const { reconcileMs, idleStopMs } = poolTimings();
@@ -22631,19 +22960,20 @@ function startPoolLoop(run3, sup, record) {
22631
22960
  const timer = setInterval(() => void tick(), reconcileMs);
22632
22961
  return () => clearInterval(timer);
22633
22962
  }
22963
+ var MODEL_REFRESH_MS = 15e3;
22634
22964
  async function nodeUpCommand(specFile, opts = {}) {
22635
22965
  try {
22636
- const specPath = resolve7(specFile);
22637
- const specRealpath = realpathSync5.native(specPath);
22638
- const parsed = parseSpec(readFileSync12(specPath, "utf-8"), dirname11(specPath));
22966
+ const specPath = resolve8(specFile);
22967
+ const specRealpath = realpathSync6.native(specPath);
22968
+ const parsed = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
22639
22969
  const previous = readState();
22640
22970
  if (previous && previous.supervisorPid !== process.pid && isAlive(previous.supervisorPid)) {
22641
22971
  throw new Error(`a node is already running (pid ${previous.supervisorPid}, ${previous.specPath}). Stop it first.`);
22642
22972
  }
22643
22973
  for (const a of parsed.agents) {
22644
- if (!existsSync11(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
22974
+ if (!existsSync12(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
22645
22975
  }
22646
- const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync5.native(a.worktree) }));
22976
+ const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync6.native(a.worktree) }));
22647
22977
  const pools = parsed.pools.map((pool) => {
22648
22978
  const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
22649
22979
  return { pool, members: poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] })) };
@@ -22673,6 +23003,20 @@ async function nodeUpCommand(specFile, opts = {}) {
22673
23003
  const inviteCodes = collectInvites(spec.agents.map((a) => a.name), opts.invite ?? [], process.env);
22674
23004
  const creds = await ensureIdentities(spec, inviteCodes, specRealpath);
22675
23005
  const runnable = spec.agents.filter((a) => creds.has(a.name));
23006
+ writeModelFiles(spec.agents);
23007
+ let modelError = "";
23008
+ const refreshModels = setInterval(() => {
23009
+ try {
23010
+ const fresh = new Map(allAgents(parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath))).map((a) => [a.name, a.model]));
23011
+ writeModelFiles(spec.agents.map((a) => ({ name: a.name, model: fresh.get(a.name) })));
23012
+ modelError = "";
23013
+ } catch (err) {
23014
+ const message = err instanceof Error ? err.message : String(err);
23015
+ if (message !== modelError) console.log(source_default.yellow(` ! spec not re-read for models: ${message}`));
23016
+ modelError = message;
23017
+ }
23018
+ }, MODEL_REFRESH_MS);
23019
+ refreshModels.unref();
22676
23020
  if (runnable.length === 0) throw new Error("no agent has credentials, nothing to start");
22677
23021
  const mcp = resolveMcpServer({ here: HERE });
22678
23022
  for (const a of runnable) prepareAgent(a, creds.get(a.name), mcp);
@@ -22711,6 +23055,12 @@ async function nodeUpCommand(specFile, opts = {}) {
22711
23055
  process.on("SIGTERM", shutdown);
22712
23056
  process.on("SIGHUP", shutdown);
22713
23057
  sup.start();
23058
+ const byRepo = /* @__PURE__ */ new Map();
23059
+ for (const a of runnable) {
23060
+ const c = creds.get(a.name);
23061
+ byRepo.set(c.repoId, [...byRepo.get(c.repoId) ?? [], { name: a.name, creds: c }]);
23062
+ }
23063
+ for (const group of byRepo.values()) stopLoops.push(startActivityLoop(group, sup));
22714
23064
  for (const { pool, members } of pools) {
22715
23065
  const ready = members.filter((m) => creds.has(m.name)).map((agent) => ({ agent, creds: creds.get(agent.name) }));
22716
23066
  if (ready.length === 0) {
@@ -22731,9 +23081,9 @@ async function nodeUpCommand(specFile, opts = {}) {
22731
23081
  }
22732
23082
  async function nodePruneCommand(specFile, opts = {}) {
22733
23083
  try {
22734
- const specPath = resolve7(specFile);
22735
- const specRealpath = realpathSync5.native(specPath);
22736
- const spec = parseSpec(readFileSync12(specPath, "utf-8"), dirname11(specPath));
23084
+ const specPath = resolve8(specFile);
23085
+ const specRealpath = realpathSync6.native(specPath);
23086
+ const spec = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
22737
23087
  const { orphans: names, unattributed } = classifyStored(allAgents(spec).map((a) => a.name), specRealpath);
22738
23088
  if (names.length === 0) {
22739
23089
  console.log("No stored credentials are missing from the spec.");
@@ -22839,6 +23189,7 @@ var token = program2.command("token").description("Manage your agent's API token
22839
23189
  token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
22840
23190
  token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
22841
23191
  token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
23192
+ program2.command("onboard <github-url>").description("Set a GitHub repo up for pitboss: 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("--writer <runtime>", "Runtime for the writer pool (default claude)").option("--reviewer <runtime>", "Runtime for the reviewer; must differ from the writers (default copilot, or claude for non-claude writers)").option("--pool-max <n>", "Writer pool size (default 2)").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
22842
23193
  var node = program2.command("node").description("Run this machine's agents headless and keep them running");
22843
23194
  node.command("up <spec>").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them)").option(
22844
23195
  "--invite <name=code>",