@pitboss/cli 0.4.3 → 0.4.4

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((resolve7, reject) => {
10867
+ var detectFile = (filepath, opts = {}) => new Promise((resolve8, 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
- resolve7((0, exports.detect)(buffer));
10877
+ resolve8((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 resolve7;
17284
+ let resolve8;
17285
17285
  let reject;
17286
17286
  const promise = new Promise((res, rej) => {
17287
- resolve7 = res;
17287
+ resolve8 = res;
17288
17288
  reject = rej;
17289
17289
  });
17290
- return { promise, resolve: resolve7, reject };
17290
+ return { promise, resolve: resolve8, 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: resolve7, reject } = PromisePolyfill.withResolver();
17327
+ const { promise, resolve: resolve8, 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
- resolve7(value);
17358
+ resolve8(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
- resolve7(value);
17381
+ resolve8(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((resolve7, reject) => {
17536
+ const promise = new Promise((resolve8, 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
- resolve7();
17547
+ resolve8();
17548
17548
  });
17549
17549
  } catch (launchError) {
17550
17550
  reject(new LaunchEditorError(launchError));
@@ -18343,7 +18343,7 @@ async function taskCreateCommand(options) {
18343
18343
  if (options.reviewBy) {
18344
18344
  verifyKind = "reviewer_agent";
18345
18345
  } else if (options.verifyKind) {
18346
- if (!["shell", "file_exists", "thread_concluded", "reviewer_agent", "human_approval"].includes(options.verifyKind)) {
18346
+ if (!["shell", "file_exists", "thread_concluded", "reviewer_agent", "human_approval", "ci_passed"].includes(options.verifyKind)) {
18347
18347
  console.error(source_default.red(`Invalid --verify-kind "${options.verifyKind}".`));
18348
18348
  process.exit(1);
18349
18349
  }
@@ -18357,6 +18357,9 @@ async function taskCreateCommand(options) {
18357
18357
  process.exit(1);
18358
18358
  }
18359
18359
  verifyReviewerId = await resolveAgentRef(client, config.repoId, reviewerRef);
18360
+ } else if (verifyKind === "ci_passed" && !options.verifyPath) {
18361
+ console.error(source_default.red("--verify-kind ci_passed requires --verify-path <sha>."));
18362
+ process.exit(1);
18360
18363
  } else if (options.verifyReviewer) {
18361
18364
  console.error(source_default.red("--verify-reviewer is only valid with --verify-kind reviewer_agent."));
18362
18365
  process.exit(1);
@@ -18957,10 +18960,10 @@ async function readWake(stream, { idleMs = 1e3, stallMs = 75e3 } = {}) {
18957
18960
  }
18958
18961
  if (next.done) return wake;
18959
18962
  buffer += decoder.decode(next.value, { stream: true });
18960
- let sep2;
18961
- while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
18962
- const { id, event } = parseFrame(buffer.slice(0, sep2));
18963
- buffer = buffer.slice(sep2 + 2);
18963
+ let sep3;
18964
+ while ((sep3 = buffer.indexOf("\n\n")) !== -1) {
18965
+ const { id, event } = parseFrame(buffer.slice(0, sep3));
18966
+ buffer = buffer.slice(sep3 + 2);
18964
18967
  if (id && EVENT_ID.test(id)) wake.lastId = id;
18965
18968
  if (event?.kind === "replay.truncated" && !wake.first) {
18966
18969
  wake.truncated = true;
@@ -20340,10 +20343,10 @@ async function consumeEventStream(stream, onEvent) {
20340
20343
  const { done, value } = await reader.read();
20341
20344
  if (done) break;
20342
20345
  buffer += decoder.decode(value, { stream: true });
20343
- let sep2;
20344
- while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
20345
- const event = parseSseFrame(buffer.slice(0, sep2));
20346
- buffer = buffer.slice(sep2 + 2);
20346
+ let sep3;
20347
+ while ((sep3 = buffer.indexOf("\n\n")) !== -1) {
20348
+ const event = parseSseFrame(buffer.slice(0, sep3));
20349
+ buffer = buffer.slice(sep3 + 2);
20347
20350
  if (event) onEvent(event);
20348
20351
  }
20349
20352
  }
@@ -20596,13 +20599,13 @@ function hookCursorPromptCommand(opts) {
20596
20599
  }
20597
20600
 
20598
20601
  // src/commands/node.ts
20599
- import { execFileSync as execFileSync6, spawn as spawnChild } from "node:child_process";
20600
- import { closeSync, existsSync as existsSync10, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync12, realpathSync as realpathSync4 } from "node:fs";
20601
- import { dirname as dirname10, resolve as resolve6 } from "node:path";
20602
+ import { execFileSync as execFileSync7, spawn as spawnChild } from "node:child_process";
20603
+ import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync12, realpathSync as realpathSync5 } from "node:fs";
20604
+ import { dirname as dirname11, resolve as resolve7 } from "node:path";
20602
20605
  import { fileURLToPath as fileURLToPath3 } from "node:url";
20603
20606
 
20604
20607
  // src/node/spec.ts
20605
- import { resolve as resolve4 } from "node:path";
20608
+ import { join as join14, resolve as resolve4, sep as sep2 } from "node:path";
20606
20609
 
20607
20610
  // ../claude-worker/src/session.ts
20608
20611
  import { spawn as spawn2 } from "child_process";
@@ -20754,7 +20757,7 @@ var HANDOFF_RULES = `## Handoff discipline
20754
20757
  relevant paths, branch name, and what you did or found. The next session has the
20755
20758
  thread, not your head.
20756
20759
  - \`mcp__relai__create_task\` only when a human explicitly asks you to file a **new**
20757
- piece of work. Say in the description that it is a proposal an orchestrator must
20760
+ piece of work, or when you hold an order's planning task (see Factory orders below). Say in the description that it is a proposal an orchestrator must
20758
20761
  commit. Do not attach roundNumber or parentTaskId bookkeeping, and do not use it
20759
20762
  to chain reviewer/writer/tester/devops work.`;
20760
20763
  function specializationBlock(spec) {
@@ -20870,6 +20873,57 @@ Handle CI/CD, infrastructure, build/deploy configuration.
20870
20873
  throw new Error(`Unknown specialization "${spec}" \u2014 valid values: reviewer, architect, writer, tester, devops`);
20871
20874
  }
20872
20875
  }
20876
+ var ORDER_RULES = `## Factory orders
20877
+
20878
+ An order is a feature request the system builds end to end. Its tasks carry
20879
+ \`metadata.orderId\` and \`metadata.orderRole\`. For a task with \`metadata.orderRole\`, the
20880
+ instructions below override your role rules and the branch rules above. Read the order with
20881
+ \`mcp__relai__get_order\` (id = \`metadata.orderId\`). The order branch is \`order/<orderId>\`.
20882
+ Find the default branch with \`git remote show origin\`.
20883
+
20884
+ **orderRole "planning"** (you are the order's planner):
20885
+ 1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it.
20886
+ 2. Create the branch and PR:
20887
+ \`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
20888
+ \`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
20889
+ then \`gh pr create --draft --base <default> --head order/<orderId> --title "<feature>" --body "Order <orderId>"\`.
20890
+ Record both with \`mcp__relai__update_order\` (\`branch\`, \`prUrl\`).
20891
+ 3. Build tasks: \`mcp__relai__create_task\` for each piece of work, with \`epicId\` = the order's
20892
+ \`epicId\`, \`assignedTo: "@auto"\`, and
20893
+ \`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
20894
+ \`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
20895
+ the router never hands out a task whose tag no agent matches. Make them small
20896
+ and independently testable so several agents can work at once. Use \`blockedBy\` only where
20897
+ one task truly needs another's result. Each description is self-contained: what to change,
20898
+ which files, how to test it, and the acceptance criteria.
20899
+ 4. Gate tasks, blocked by every build task, \`assignedTo: "@auto"\`,
20900
+ \`metadata: { orderId, orderRole: "gate" }\`:
20901
+ - \`human_approval\` in acceptance: a task "Approve: <feature>" with \`verifyKind: "human_approval"\`.
20902
+ - \`reviewer_agent\` in acceptance: a task "Review: <feature>" with \`verifyKind: "reviewer_agent"\`
20903
+ and \`verifyReviewerId\` set to a reviewer from \`mcp__relai__list_agents\`.
20904
+ - \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
20905
+ 5. One merge task, blocked by every gate task (or every build task when there are no gates),
20906
+ \`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
20907
+ 6. Post the plan (task ids and titles) on the planning task's thread and complete it.
20908
+
20909
+ **orderRole "build"**:
20910
+ - \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
20911
+ test and commit. Do not create another branch or PR.
20912
+ - Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
20913
+ repeating up to 5 times if the push is rejected. Never force-push.
20914
+ - Hand off on the task's thread with the commit sha and what you tested, then complete.
20915
+
20916
+ **orderRole "gate"**:
20917
+ - Check every build task in the order's Epic is completed, then post the PR link on the task's
20918
+ thread and complete it. The gate decides from there: the owner approves a \`human_approval\`
20919
+ gate on the dashboard and the named reviewer decides a \`reviewer_agent\` gate.
20920
+
20921
+ **orderRole "merge"**:
20922
+ 1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
20923
+ 2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
20924
+ gh pr view --json headRefOid>\`, post the merge commit on the thread, and complete.
20925
+ 3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
20926
+ After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
20873
20927
  function buildPrompt(config) {
20874
20928
  return [
20875
20929
  BASE_TASK_LOOP(config.agentId, config.repoPath),
@@ -20878,7 +20932,9 @@ function buildPrompt(config) {
20878
20932
  "",
20879
20933
  SAME_TASK_RULES(config.maxTaskRounds),
20880
20934
  "",
20881
- HANDOFF_RULES
20935
+ HANDOFF_RULES,
20936
+ "",
20937
+ ORDER_RULES
20882
20938
  ].join("\n");
20883
20939
  }
20884
20940
 
@@ -21045,7 +21101,7 @@ async function runClaudeSession(config) {
21045
21101
  const mcpConfigPath = writeMcpConfig(config);
21046
21102
  const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
21047
21103
  try {
21048
- return await new Promise((resolve7, reject) => {
21104
+ return await new Promise((resolve8, reject) => {
21049
21105
  const args = [
21050
21106
  "--print",
21051
21107
  "--mcp-config",
@@ -21125,7 +21181,7 @@ async function runClaudeSession(config) {
21125
21181
  const detail = (resultError || stderrOutput || "").trim();
21126
21182
  reject(new Error(`claude exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21127
21183
  } else {
21128
- resolve7(unique);
21184
+ resolve8(unique);
21129
21185
  }
21130
21186
  };
21131
21187
  finish.then(settle, settle);
@@ -21205,7 +21261,7 @@ async function runCursorSession(config) {
21205
21261
  const args = cursorArgs(config.repoPath, buildPrompt(config));
21206
21262
  console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
21207
21263
  const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
21208
- return new Promise((resolve7, reject) => {
21264
+ return new Promise((resolve8, reject) => {
21209
21265
  const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21210
21266
  console.log(`[cursor-worker] Process PID: ${proc.pid}`);
21211
21267
  const toolsUsed = [];
@@ -21244,7 +21300,7 @@ async function runCursorSession(config) {
21244
21300
  const detail = (resultError || stderrOutput).trim();
21245
21301
  reject(new Error(`cursor-agent exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21246
21302
  } else {
21247
- resolve7([...new Set(toolsUsed)]);
21303
+ resolve8([...new Set(toolsUsed)]);
21248
21304
  }
21249
21305
  };
21250
21306
  finish.then(settle, settle);
@@ -21292,10 +21348,53 @@ var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devop
21292
21348
 
21293
21349
  // src/node/spec.ts
21294
21350
  var DEFAULT_API2 = "https://api.pitboss.dev";
21351
+ var MAX_POOL_SIZE = 32;
21295
21352
  var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
21353
+ var POOL_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,59}$/;
21296
21354
  function inviteEnvKey(name) {
21297
21355
  return `PITBOSS_NODE_INVITE_${name.toUpperCase().replace(/[.-]/g, "_")}`;
21298
21356
  }
21357
+ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(sep2) ? dir : dir + sep2);
21358
+ function poolMembers(pool) {
21359
+ return Array.from({ length: pool.max }, (_, i) => {
21360
+ const name = `${pool.pool}-${i + 1}`;
21361
+ return { name, runtime: pool.runtime, worktree: join14(pool.worktreeRoot, name), specialization: pool.specialization };
21362
+ });
21363
+ }
21364
+ var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
21365
+ function runtimeOf(a, i) {
21366
+ if (!RUNTIMES3.includes(a.runtime)) {
21367
+ throw new Error(`agents[${i}]: runtime "${String(a.runtime)}" is not supported (have: ${RUNTIMES3.join(", ")})`);
21368
+ }
21369
+ return a.runtime;
21370
+ }
21371
+ function specializationOf(a, i) {
21372
+ const specialization = a.specialization === void 0 ? "writer" : a.specialization;
21373
+ if (!VALID_SPECIALIZATIONS.includes(specialization)) {
21374
+ throw new Error(`agents[${i}]: specialization "${String(specialization)}" is not one of ${VALID_SPECIALIZATIONS.join(", ")}`);
21375
+ }
21376
+ return specialization;
21377
+ }
21378
+ function parsePool(a, i, specDir) {
21379
+ if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
21380
+ if (typeof a.pool !== "string" || !POOL_NAME.test(a.pool)) throw new Error(`agents[${i}].pool must be letters, digits, '.', '_' or '-' (at most 60)`);
21381
+ if (typeof a.base !== "string" || !a.base) throw new Error(`agents[${i}].base is required: the clone pool worktrees are created from`);
21382
+ if (typeof a.worktreeRoot !== "string" || !a.worktreeRoot) throw new Error(`agents[${i}].worktreeRoot is required`);
21383
+ const runtime = runtimeOf(a, i);
21384
+ const specialization = specializationOf(a, i);
21385
+ const max = a.max;
21386
+ if (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > MAX_POOL_SIZE) {
21387
+ throw new Error(`agents[${i}].max must be an integer from 1 to ${MAX_POOL_SIZE}`);
21388
+ }
21389
+ const min = a.min === void 0 ? 0 : a.min;
21390
+ if (typeof min !== "number" || !Number.isInteger(min) || min < 0 || min > max) {
21391
+ throw new Error(`agents[${i}].min must be an integer from 0 to max`);
21392
+ }
21393
+ const base2 = resolve4(specDir, a.base);
21394
+ const worktreeRoot = resolve4(specDir, a.worktreeRoot);
21395
+ if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
21396
+ return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, min, max };
21397
+ }
21299
21398
  function parseSpec(text, specDir) {
21300
21399
  let raw;
21301
21400
  try {
@@ -21309,23 +21408,30 @@ function parseSpec(text, specDir) {
21309
21408
  throw new Error("api must be https (plain http only for localhost)");
21310
21409
  }
21311
21410
  if (!Array.isArray(body.agents) || body.agents.length === 0) throw new Error("spec needs at least one agent");
21312
- const agents = body.agents.map((entry, i) => {
21411
+ const agents = [];
21412
+ const pools = [];
21413
+ body.agents.forEach((entry, i) => {
21313
21414
  const a = entry ?? {};
21415
+ if (a.pool !== void 0) {
21416
+ pools.push(parsePool(a, i, specDir));
21417
+ return;
21418
+ }
21314
21419
  if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
21315
21420
  if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
21316
- if (!RUNTIMES3.includes(a.runtime)) {
21317
- throw new Error(`agents[${i}]: runtime "${String(a.runtime)}" is not supported (have: ${RUNTIMES3.join(", ")})`);
21318
- }
21319
- const specialization = a.specialization === void 0 ? "writer" : a.specialization;
21320
- if (!VALID_SPECIALIZATIONS.includes(specialization)) {
21321
- throw new Error(`agents[${i}]: specialization "${String(specialization)}" is not one of ${VALID_SPECIALIZATIONS.join(", ")}`);
21322
- }
21323
- return { name: a.name, runtime: a.runtime, worktree: resolve4(specDir, a.worktree), specialization };
21421
+ const runtime = runtimeOf(a, i);
21422
+ agents.push({ name: a.name, runtime, worktree: resolve4(specDir, a.worktree), specialization: specializationOf(a, i) });
21324
21423
  });
21424
+ for (const p of pools) {
21425
+ for (const a of agents) {
21426
+ if (isWithin(p.worktreeRoot, a.worktree)) {
21427
+ throw new Error(`pool "${p.pool}" worktreeRoot ${p.worktreeRoot} is inside agent "${a.name}"'s worktree`);
21428
+ }
21429
+ }
21430
+ }
21325
21431
  const names = /* @__PURE__ */ new Set();
21326
21432
  const worktrees = /* @__PURE__ */ new Map();
21327
21433
  const envKeys = /* @__PURE__ */ new Map();
21328
- for (const a of agents) {
21434
+ for (const a of allAgents({ api, agents, pools })) {
21329
21435
  if (names.has(a.name.toLowerCase())) throw new Error(`agent name "${a.name}" appears twice`);
21330
21436
  names.add(a.name.toLowerCase());
21331
21437
  const envKey = inviteEnvKey(a.name);
@@ -21336,17 +21442,17 @@ function parseSpec(text, specDir) {
21336
21442
  if (other) throw new Error(`agents "${other}" and "${a.name}" share the worktree ${a.worktree}; each needs its own`);
21337
21443
  worktrees.set(a.worktree, a.name);
21338
21444
  }
21339
- return { api: api.replace(/\/+$/, ""), agents };
21445
+ return { api: api.replace(/\/+$/, ""), agents, pools };
21340
21446
  }
21341
21447
 
21342
21448
  // src/node/store.ts
21343
21449
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync11, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
21344
- import { dirname as dirname8, join as join14 } from "node:path";
21450
+ import { dirname as dirname8, join as join15 } from "node:path";
21345
21451
  import { randomBytes as randomBytes4 } from "node:crypto";
21346
- var nodeDir = () => join14(configDir(), "node");
21347
- var credsPath = (name) => join14(nodeDir(), "agents", `${name}.json`);
21348
- var statePath2 = () => join14(nodeDir(), "state.json");
21349
- var logPath = (name) => join14(nodeDir(), "logs", `${name}.log`);
21452
+ var nodeDir = () => join15(configDir(), "node");
21453
+ var credsPath = (name) => join15(nodeDir(), "agents", `${name}.json`);
21454
+ var statePath2 = () => join15(nodeDir(), "state.json");
21455
+ var logPath = (name) => join15(nodeDir(), "logs", `${name}.log`);
21350
21456
  function writePrivate(path3, value) {
21351
21457
  mkdirSync6(dirname8(path3), { recursive: true, mode: 448 });
21352
21458
  const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
@@ -21364,7 +21470,7 @@ var removeCreds = (name) => {
21364
21470
  if (existsSync8(path3)) rmSync4(path3);
21365
21471
  };
21366
21472
  var listStoredAgentNames = () => {
21367
- const dir = join14(nodeDir(), "agents");
21473
+ const dir = join15(nodeDir(), "agents");
21368
21474
  if (!existsSync8(dir)) return [];
21369
21475
  return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
21370
21476
  };
@@ -21577,14 +21683,45 @@ var Supervisor = class {
21577
21683
  failures: 0,
21578
21684
  startedMs: 0,
21579
21685
  timer: null,
21580
- state: { pid: null, status: "starting", restarts: 0, startedAt: null, lastExit: null, nextStartAt: null }
21686
+ held: spec.onDemand === true,
21687
+ state: { pid: null, status: spec.onDemand ? "stopped" : "starting", restarts: 0, startedAt: null, lastExit: null, nextStartAt: null }
21581
21688
  }));
21582
21689
  }
21583
21690
  opts;
21584
21691
  slots;
21585
21692
  stopping = false;
21586
21693
  start() {
21587
- for (const slot of this.slots) this.launch(slot);
21694
+ for (const slot of this.slots) if (!slot.held) this.launch(slot);
21695
+ }
21696
+ isActive(name) {
21697
+ return this.slot(name).held === false;
21698
+ }
21699
+ startAgent(name) {
21700
+ const slot = this.slot(name);
21701
+ if (!slot.held || this.stopping) return;
21702
+ slot.held = false;
21703
+ slot.failures = 0;
21704
+ this.launch(slot);
21705
+ }
21706
+ async stopAgent(name) {
21707
+ const slot = this.slot(name);
21708
+ slot.held = true;
21709
+ if (slot.timer) clearTimeout(slot.timer);
21710
+ slot.timer = null;
21711
+ const child = slot.child;
21712
+ if (!child) {
21713
+ slot.state = { ...slot.state, pid: null, status: "stopped", nextStartAt: null };
21714
+ this.emit();
21715
+ return;
21716
+ }
21717
+ const exited = new Promise((done) => child.on("exit", () => done()));
21718
+ child.kill("SIGTERM");
21719
+ await exited;
21720
+ }
21721
+ slot(name) {
21722
+ const slot = this.slots.find((s) => s.spec.name === name);
21723
+ if (!slot) throw new Error(`no agent named ${name}`);
21724
+ return slot;
21588
21725
  }
21589
21726
  snapshot() {
21590
21727
  return Object.fromEntries(this.slots.map((s) => [s.spec.name, { ...s.state }]));
@@ -21634,8 +21771,9 @@ var Supervisor = class {
21634
21771
  slot.child = null;
21635
21772
  const at = (/* @__PURE__ */ new Date()).toISOString();
21636
21773
  const lastExit = { code, signal, at, ...error ? { error } : {} };
21637
- if (this.stopping) {
21774
+ if (this.stopping || slot.held) {
21638
21775
  slot.state = { ...slot.state, pid: null, status: "stopped", lastExit, nextStartAt: null };
21776
+ if (slot.held && !this.stopping) this.emit();
21639
21777
  return;
21640
21778
  }
21641
21779
  slot.failures = slot.startedMs && Date.now() - slot.startedMs >= this.opts.stableMs ? 1 : slot.failures + 1;
@@ -21658,27 +21796,27 @@ var Supervisor = class {
21658
21796
 
21659
21797
  // src/node/entries.ts
21660
21798
  import { existsSync as existsSync9 } from "node:fs";
21661
- import { dirname as dirname9, join as join15, resolve as resolve5 } from "node:path";
21799
+ import { dirname as dirname9, join as join16, resolve as resolve5 } from "node:path";
21662
21800
  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.";
21663
21801
  function checkoutWorker(dir) {
21664
21802
  return {
21665
- command: join15(dir, "node_modules", ".bin", "tsx"),
21666
- args: [join15(dir, "src", "index.ts")]
21803
+ command: join16(dir, "node_modules", ".bin", "tsx"),
21804
+ args: [join16(dir, "src", "index.ts")]
21667
21805
  };
21668
21806
  }
21669
21807
  function checkoutMcp(dir, execPath) {
21670
21808
  return {
21671
21809
  command: execPath,
21672
21810
  args: [
21673
- join15(dir, "node_modules", "tsx", "dist", "cli.mjs"),
21674
- join15(dir, "src", "index.ts")
21811
+ join16(dir, "node_modules", "tsx", "dist", "cli.mjs"),
21812
+ join16(dir, "src", "index.ts")
21675
21813
  ]
21676
21814
  };
21677
21815
  }
21678
21816
  function walkFor(here, packageName, exists) {
21679
21817
  for (let dir = dirname9(here); dirname9(dir) !== dir; dir = dirname9(dir)) {
21680
- const candidate = join15(dir, packageName);
21681
- if (exists(join15(candidate, "src", "index.ts"))) return candidate;
21818
+ const candidate = join16(dir, packageName);
21819
+ if (exists(join16(candidate, "src", "index.ts"))) return candidate;
21682
21820
  }
21683
21821
  return null;
21684
21822
  }
@@ -21690,10 +21828,10 @@ function resolveEventWorker(opts) {
21690
21828
  if (override) {
21691
21829
  const resolved = resolve5(override);
21692
21830
  if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
21693
- if (exists(join15(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
21831
+ if (exists(join16(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
21694
21832
  throw new Error(MISSING_WORKER);
21695
21833
  }
21696
- const bundled = join15(dirname9(opts.here), "event-worker.js");
21834
+ const bundled = join16(dirname9(opts.here), "event-worker.js");
21697
21835
  if (exists(bundled)) return { command: execPath, args: [bundled] };
21698
21836
  const dir = walkFor(opts.here, "event-worker", exists);
21699
21837
  if (dir) return checkoutWorker(dir);
@@ -21702,7 +21840,7 @@ function resolveEventWorker(opts) {
21702
21840
  function resolveMcpServer(opts) {
21703
21841
  const execPath = opts.execPath ?? process.execPath;
21704
21842
  const exists = opts.exists ?? existsSync9;
21705
- const bundled = join15(dirname9(opts.here), "mcp.js");
21843
+ const bundled = join16(dirname9(opts.here), "mcp.js");
21706
21844
  if (exists(bundled)) return { command: execPath, args: [bundled] };
21707
21845
  const dir = walkFor(opts.here, "mcp-server", exists);
21708
21846
  if (dir) return checkoutMcp(dir, execPath);
@@ -21722,7 +21860,18 @@ function describeState(state, alive = isAlive) {
21722
21860
  const lastExit = a.lastExit ? a.lastExit.signal ? `signal ${a.lastExit.signal}` : a.lastExit.error ?? `code ${a.lastExit.code}` : null;
21723
21861
  return { name, status, pid: a.pid, restarts: a.restarts, lastExit };
21724
21862
  });
21725
- return { supervised, agents };
21863
+ const byName = new Map(agents.map((a) => [a.name, a]));
21864
+ const pools = Object.entries(state.pools ?? {}).map(([name, p]) => ({
21865
+ name,
21866
+ members: p.members,
21867
+ min: p.min,
21868
+ max: p.max,
21869
+ ready: p.ready,
21870
+ checkedAt: p.checkedAt,
21871
+ error: p.error ?? null,
21872
+ running: p.members.filter((m) => byName.get(m)?.status === "running").length
21873
+ }));
21874
+ return { supervised, agents, pools };
21726
21875
  }
21727
21876
  function orphanedWorkers(state, alive, inspect) {
21728
21877
  if (alive(state.supervisorPid)) return [];
@@ -21798,18 +21947,178 @@ function loadPruneTargets(names) {
21798
21947
  });
21799
21948
  }
21800
21949
 
21950
+ // src/node/pool.ts
21951
+ import { execFileSync as execFileSync6 } from "node:child_process";
21952
+ import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as mkdirSync7, realpathSync as realpathSync4 } from "node:fs";
21953
+ import { basename as basename2, dirname as dirname10, join as join17, resolve as resolve6 } from "node:path";
21954
+ var IDLE_STOP_MS = 10 * 6e4;
21955
+ var RECONCILE_MS = 15e3;
21956
+ var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
21957
+ var REMARK_AFTER_MS = 24 * 60 * 6e4;
21958
+ function positiveMs(raw, fallback2) {
21959
+ const n = Number(raw);
21960
+ return raw && Number.isInteger(n) && n > 0 ? n : fallback2;
21961
+ }
21962
+ function poolTimings(env2 = process.env) {
21963
+ return {
21964
+ reconcileMs: positiveMs(env2.PITBOSS_POOL_RECONCILE_MS, RECONCILE_MS),
21965
+ idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
21966
+ };
21967
+ }
21968
+ function reconcilePool({ pending, members, min, max, idleStopMs }) {
21969
+ const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
21970
+ const running = members.filter((m) => m.running);
21971
+ const stopped = members.filter((m) => !m.running);
21972
+ let count = running.length;
21973
+ const start = [];
21974
+ const take = (m) => {
21975
+ start.push(m.name);
21976
+ count++;
21977
+ };
21978
+ for (const m of stopped) {
21979
+ if (count >= max) break;
21980
+ if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
21981
+ }
21982
+ const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
21983
+ let demand = pending - running.filter(isIdle).length;
21984
+ for (const m of spare()) {
21985
+ if (demand <= 0 || count >= max) break;
21986
+ take(m);
21987
+ demand--;
21988
+ }
21989
+ for (const m of spare()) {
21990
+ if (count >= min || count >= max) break;
21991
+ take(m);
21992
+ }
21993
+ if (start.length > 0) return { start, stop: [] };
21994
+ const stop = [];
21995
+ let surplus = running.filter(isIdle).length - pending;
21996
+ const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
21997
+ for (const m of eligible) {
21998
+ if (surplus <= 0 || count <= min) break;
21999
+ stop.push(m.name);
22000
+ count--;
22001
+ surplus--;
22002
+ }
22003
+ return { start, stop };
22004
+ }
22005
+ function summarizeTasks(ready, held, members) {
22006
+ const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
22007
+ const counts = Object.fromEntries(
22008
+ members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
22009
+ );
22010
+ const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
22011
+ let readyAssigned = 0;
22012
+ for (const t of ready) {
22013
+ const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
22014
+ if (!name) continue;
22015
+ counts[name].readyAssigned++;
22016
+ readyAssigned++;
22017
+ }
22018
+ for (const t of held) {
22019
+ const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
22020
+ if (!name) continue;
22021
+ if (t.status === "assigned") counts[name].assigned++;
22022
+ if (t.status === "in_progress") counts[name].inProgress++;
22023
+ }
22024
+ return { pending, ready: pending + readyAssigned, members: counts };
22025
+ }
22026
+ function poolRootProblem(root, protectedPaths) {
22027
+ for (const p of protectedPaths) {
22028
+ if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
22029
+ }
22030
+ return null;
22031
+ }
22032
+ function memberPathProblem(s) {
22033
+ if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
22034
+ if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
22035
+ if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
22036
+ return null;
22037
+ }
22038
+ function realpathAllowMissing(path3) {
22039
+ const abs = resolve6(path3);
22040
+ if (existsSync10(abs)) return realpathSync4.native(abs);
22041
+ return join17(realpathAllowMissing(dirname10(abs)), basename2(abs));
22042
+ }
22043
+ function run2(cwd, args) {
22044
+ try {
22045
+ return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
22046
+ } catch (err) {
22047
+ const stderr = err.stderr?.toString().trim();
22048
+ throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
22049
+ }
22050
+ }
22051
+ var commonDir = (cwd) => {
22052
+ const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
22053
+ return dir ? realpathSync4.native(dir) : null;
22054
+ };
22055
+ function defaultBranch(cwd) {
22056
+ const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
22057
+ let ref = head();
22058
+ if (!ref) {
22059
+ git(["remote", "set-head", "origin", "--auto"], cwd);
22060
+ ref = head();
22061
+ }
22062
+ if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
22063
+ return ref.slice("origin/".length);
22064
+ }
22065
+ function ensurePoolWorktrees(pool, otherWorktrees = []) {
22066
+ if (!existsSync10(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
22067
+ const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
22068
+ const baseCommon = commonDir(pool.base);
22069
+ if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
22070
+ const root = realpathAllowMissing(pool.worktreeRoot);
22071
+ const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
22072
+ if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
22073
+ const members = poolMembers({ ...pool, worktreeRoot: root });
22074
+ for (const m of members) {
22075
+ let symlink = false;
22076
+ try {
22077
+ symlink = lstatSync3(m.worktree).isSymbolicLink();
22078
+ } catch {
22079
+ symlink = false;
22080
+ }
22081
+ const exists = !symlink && existsSync10(m.worktree);
22082
+ const problem = memberPathProblem({
22083
+ path: m.worktree,
22084
+ symlink,
22085
+ exists,
22086
+ baseCommonDir: baseCommon,
22087
+ commonDir: exists ? commonDir(m.worktree) : null,
22088
+ tracked: !exists && !symlink && isTracked(m.worktree)
22089
+ });
22090
+ if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
22091
+ }
22092
+ const missing = members.filter((m) => !existsSync10(m.worktree));
22093
+ if (missing.length > 0) {
22094
+ mkdirSync7(root, { recursive: true });
22095
+ run2(pool.base, ["fetch", "-q", "origin"]);
22096
+ const branch = defaultBranch(pool.base);
22097
+ run2(pool.base, ["worktree", "prune"]);
22098
+ for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
22099
+ }
22100
+ return members.map((m) => realpathSync4.native(m.worktree));
22101
+ }
22102
+ function resetWorktree(worktree) {
22103
+ run2(worktree, ["fetch", "-q", "--prune", "origin"]);
22104
+ const target = `origin/${defaultBranch(worktree)}`;
22105
+ run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
22106
+ run2(worktree, ["reset", "-q", "--hard", target]);
22107
+ run2(worktree, ["clean", "-fdq"]);
22108
+ }
22109
+
21801
22110
  // src/commands/node.ts
21802
22111
  var HERE = fileURLToPath3(import.meta.url);
21803
22112
  function inspectProcess(pid) {
21804
22113
  try {
21805
- const out = execFileSync6("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
22114
+ const out = execFileSync7("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
21806
22115
  const match = /^(\d+)\s+(.*)$/.exec(out);
21807
22116
  return match ? { pgid: Number(match[1]), command: match[2] } : null;
21808
22117
  } catch {
21809
22118
  return null;
21810
22119
  }
21811
22120
  }
21812
- function childSpec(agent, creds, worker) {
22121
+ function childSpec(agent, creds, worker, onDemand = false) {
21813
22122
  const { ANTHROPIC_API_KEY: _key, ANTHROPIC_AUTH_TOKEN: _tok, ...inherited } = process.env;
21814
22123
  return {
21815
22124
  name: agent.name,
@@ -21817,6 +22126,7 @@ function childSpec(agent, creds, worker) {
21817
22126
  args: worker.args,
21818
22127
  cwd: agent.worktree,
21819
22128
  logPath: logPath(agent.name),
22129
+ onDemand,
21820
22130
  env: {
21821
22131
  ...inherited,
21822
22132
  API_URL: creds.api,
@@ -21841,12 +22151,12 @@ function prepareAgent(agent, creds, mcp) {
21841
22151
  excludeIfUntracked(root, target);
21842
22152
  },
21843
22153
  run(bin2, args, cwd) {
21844
- execFileSync6(bin2, args, { cwd, stdio: "ignore" });
22154
+ execFileSync7(bin2, args, { cwd, stdio: "ignore" });
21845
22155
  }
21846
22156
  });
21847
22157
  }
21848
22158
  function spawnWorker(spec) {
21849
- mkdirSync7(dirname10(spec.logPath), { recursive: true, mode: 448 });
22159
+ mkdirSync8(dirname11(spec.logPath), { recursive: true, mode: 448 });
21850
22160
  const log = openSync(spec.logPath, "a", 384);
21851
22161
  try {
21852
22162
  const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
@@ -21865,19 +22175,108 @@ function spawnWorker(spec) {
21865
22175
  closeSync(log);
21866
22176
  }
21867
22177
  }
22178
+ async function fetchTasks(creds, query) {
22179
+ const res = await getJson(`${creds.api}/tasks?repoId=${encodeURIComponent(creds.repoId)}&${query}`, creds.token);
22180
+ if (res.status !== 200 || !Array.isArray(res.payload.data)) throw new Error(`GET /tasks?${query} returned HTTP ${res.status}`);
22181
+ return res.payload.data;
22182
+ }
22183
+ async function setAvailable(creds, available) {
22184
+ const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason: "pool member stopped by pitboss node" };
22185
+ const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/unavailable`, {
22186
+ method: "PUT",
22187
+ headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
22188
+ body: JSON.stringify(body)
22189
+ });
22190
+ if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/unavailable returned HTTP ${res.status}`);
22191
+ }
22192
+ function startPoolLoop(run3, sup, record) {
22193
+ const { pool, members } = run3;
22194
+ const { reconcileMs, idleStopMs } = poolTimings();
22195
+ const lastBusy = /* @__PURE__ */ new Map();
22196
+ const markedAt = /* @__PURE__ */ new Map();
22197
+ const state = { members: members.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null };
22198
+ const log = (line) => console.log(`${(/* @__PURE__ */ new Date()).toISOString()} pool ${pool.pool}: ${line}`);
22199
+ let busy = false;
22200
+ const markStopped = async (member) => {
22201
+ try {
22202
+ await setAvailable(member.creds, false);
22203
+ markedAt.set(member.agent.name, Date.now());
22204
+ } catch (err) {
22205
+ log(`could not mark ${member.agent.name} unavailable: ${err instanceof Error ? err.message : String(err)}`);
22206
+ }
22207
+ };
22208
+ const tick = async () => {
22209
+ if (busy) return;
22210
+ busy = true;
22211
+ try {
22212
+ for (const m of members) {
22213
+ if (!sup.isActive(m.agent.name) && Date.now() - (markedAt.get(m.agent.name) ?? 0) >= REMARK_AFTER_MS) await markStopped(m);
22214
+ }
22215
+ const reader = members.find((m) => sup.isActive(m.agent.name)) ?? members[0];
22216
+ const [ready, held] = await Promise.all([
22217
+ fetchTasks(reader.creds, "ready=true"),
22218
+ fetchTasks(reader.creds, "status=assigned,in_progress")
22219
+ ]);
22220
+ const summary = summarizeTasks(ready, held, members.map((m) => ({ name: m.agent.name, agentId: m.creds.agentId })));
22221
+ const now = Date.now();
22222
+ const views = members.map(({ agent }) => {
22223
+ const counts = summary.members[agent.name];
22224
+ if (counts.assigned + counts.inProgress > 0) lastBusy.set(agent.name, now);
22225
+ return { name: agent.name, running: sup.isActive(agent.name), ...counts, idleMs: now - (lastBusy.get(agent.name) ?? now) };
22226
+ });
22227
+ const decision = reconcilePool({ pending: summary.pending, members: views, min: pool.min, max: pool.max, idleStopMs });
22228
+ for (const name of decision.start) {
22229
+ const member = members.find((m) => m.agent.name === name);
22230
+ try {
22231
+ if (summary.members[name].inProgress === 0) resetWorktree(member.agent.worktree);
22232
+ await setAvailable(member.creds, true);
22233
+ markedAt.delete(name);
22234
+ } catch (err) {
22235
+ log(`not starting ${name}: ${err instanceof Error ? err.message : String(err)}`);
22236
+ continue;
22237
+ }
22238
+ lastBusy.set(name, Date.now());
22239
+ sup.startAgent(name);
22240
+ log(`started ${name} (ready ${summary.ready})`);
22241
+ }
22242
+ for (const name of decision.stop) {
22243
+ await sup.stopAgent(name);
22244
+ await markStopped(members.find((m) => m.agent.name === name));
22245
+ log(`stopped ${name} after ${Math.round(idleStopMs / 1e3)}s idle`);
22246
+ }
22247
+ Object.assign(state, { ready: summary.ready, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: void 0 });
22248
+ } catch (err) {
22249
+ const message = err instanceof Error ? err.message : String(err);
22250
+ if (state.error !== message) log(message);
22251
+ Object.assign(state, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: message });
22252
+ } finally {
22253
+ record(pool.pool, { ...state });
22254
+ busy = false;
22255
+ }
22256
+ };
22257
+ void tick();
22258
+ const timer = setInterval(() => void tick(), reconcileMs);
22259
+ return () => clearInterval(timer);
22260
+ }
21868
22261
  async function nodeUpCommand(specFile, opts = {}) {
21869
22262
  try {
21870
- const specPath = resolve6(specFile);
21871
- const specRealpath = realpathSync4.native(specPath);
21872
- const parsed = parseSpec(readFileSync12(specPath, "utf-8"), dirname10(specPath));
22263
+ const specPath = resolve7(specFile);
22264
+ const specRealpath = realpathSync5.native(specPath);
22265
+ const parsed = parseSpec(readFileSync12(specPath, "utf-8"), dirname11(specPath));
21873
22266
  const previous = readState();
21874
22267
  if (previous && previous.supervisorPid !== process.pid && isAlive(previous.supervisorPid)) {
21875
22268
  throw new Error(`a node is already running (pid ${previous.supervisorPid}, ${previous.specPath}). Stop it first.`);
21876
22269
  }
21877
22270
  for (const a of parsed.agents) {
21878
- if (!existsSync10(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
22271
+ if (!existsSync11(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
21879
22272
  }
21880
- const spec = { ...parsed, agents: parsed.agents.map((a) => ({ ...a, worktree: realpathSync4.native(a.worktree) })) };
22273
+ const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync5.native(a.worktree) }));
22274
+ const pools = parsed.pools.map((pool) => {
22275
+ const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
22276
+ return { pool, members: poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] })) };
22277
+ });
22278
+ const spec = { ...parsed, agents: [...plain, ...pools.flatMap((p) => p.members)] };
22279
+ const pooled = new Set(pools.flatMap((p) => p.members.map((m) => m.name)));
21881
22280
  const seen = /* @__PURE__ */ new Map();
21882
22281
  for (const a of spec.agents) {
21883
22282
  const other = seen.get(a.worktree);
@@ -21894,6 +22293,7 @@ async function nodeUpCommand(specFile, opts = {}) {
21894
22293
  }
21895
22294
  }
21896
22295
  }
22296
+ const poolStates = {};
21897
22297
  writeState({ ...base2, agents: {} });
21898
22298
  logStoredOrphans(spec.agents.map((a) => a.name), specRealpath);
21899
22299
  const worker = resolveEventWorker({ here: HERE });
@@ -21904,13 +22304,13 @@ async function nodeUpCommand(specFile, opts = {}) {
21904
22304
  const mcp = resolveMcpServer({ here: HERE });
21905
22305
  for (const a of runnable) prepareAgent(a, creds.get(a.name), mcp);
21906
22306
  const last = /* @__PURE__ */ new Map();
21907
- const sup = new Supervisor(runnable.map((a) => childSpec(a, creds.get(a.name), worker)), {
22307
+ const sup = new Supervisor(runnable.map((a) => childSpec(a, creds.get(a.name), worker, pooled.has(a.name))), {
21908
22308
  spawn: spawnWorker,
21909
22309
  baseMs: 1e3,
21910
22310
  maxMs: 6e4,
21911
22311
  stableMs: 6e4,
21912
22312
  onChange: (agents) => {
21913
- writeState({ ...base2, agents });
22313
+ writeState({ ...base2, agents, pools: poolStates });
21914
22314
  for (const [name, s] of Object.entries(agents)) {
21915
22315
  const line = s.status === "running" ? `running pid ${s.pid}` : s.status === "backoff" ? `exited (${s.lastExit?.signal ?? s.lastExit?.error ?? `code ${s.lastExit?.code}`}), restart ${s.restarts} at ${s.nextStartAt}` : s.status;
21916
22316
  if (last.get(name) === line) continue;
@@ -21919,13 +22319,17 @@ async function nodeUpCommand(specFile, opts = {}) {
21919
22319
  }
21920
22320
  }
21921
22321
  });
22322
+ const plainCount = runnable.filter((a) => !pooled.has(a.name)).length;
22323
+ const poolNote = pools.length > 0 ? `, ${pools.length} pool(s)` : "";
21922
22324
  console.log(source_default.bold(`
21923
- pitboss node: ${runnable.length} agent(s)`) + source_default.dim(` state ${statePath2()}
22325
+ pitboss node: ${plainCount} agent(s)${poolNote}`) + source_default.dim(` state ${statePath2()}
21924
22326
  `));
22327
+ const stopLoops = [];
21925
22328
  let stopping = false;
21926
22329
  const shutdown = async () => {
21927
22330
  if (stopping) return;
21928
22331
  stopping = true;
22332
+ for (const stopLoop of stopLoops) stopLoop();
21929
22333
  console.log(source_default.dim("\n stopping agents\u2026"));
21930
22334
  await sup.stop();
21931
22335
  process.exit(0);
@@ -21934,6 +22338,18 @@ async function nodeUpCommand(specFile, opts = {}) {
21934
22338
  process.on("SIGTERM", shutdown);
21935
22339
  process.on("SIGHUP", shutdown);
21936
22340
  sup.start();
22341
+ for (const { pool, members } of pools) {
22342
+ const ready = members.filter((m) => creds.has(m.name)).map((agent) => ({ agent, creds: creds.get(agent.name) }));
22343
+ if (ready.length === 0) {
22344
+ console.log(source_default.yellow(` ! pool ${pool.pool}: no member has credentials; it will not scale`));
22345
+ continue;
22346
+ }
22347
+ poolStates[pool.pool] = { members: ready.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null };
22348
+ stopLoops.push(startPoolLoop({ pool, members: ready }, sup, (name, state) => {
22349
+ poolStates[name] = state;
22350
+ writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
22351
+ }));
22352
+ }
21937
22353
  } catch (err) {
21938
22354
  console.error(source_default.red(`
21939
22355
  ${err instanceof Error ? err.message : String(err)}`));
@@ -21942,10 +22358,10 @@ async function nodeUpCommand(specFile, opts = {}) {
21942
22358
  }
21943
22359
  async function nodePruneCommand(specFile, opts = {}) {
21944
22360
  try {
21945
- const specPath = resolve6(specFile);
21946
- const specRealpath = realpathSync4.native(specPath);
21947
- const spec = parseSpec(readFileSync12(specPath, "utf-8"), dirname10(specPath));
21948
- const { orphans: names, unattributed } = classifyStored(spec.agents.map((a) => a.name), specRealpath);
22361
+ const specPath = resolve7(specFile);
22362
+ const specRealpath = realpathSync5.native(specPath);
22363
+ const spec = parseSpec(readFileSync12(specPath, "utf-8"), dirname11(specPath));
22364
+ const { orphans: names, unattributed } = classifyStored(allAgents(spec).map((a) => a.name), specRealpath);
21949
22365
  if (names.length === 0) {
21950
22366
  console.log("No stored credentials are missing from the spec.");
21951
22367
  if (unattributed.length > 0) console.log(`unattributed, not pruned: ${unattributed.join(", ")}`);
@@ -21989,9 +22405,9 @@ function nodeStatusCommand(opts) {
21989
22405
  console.log("No node has run on this machine.");
21990
22406
  return;
21991
22407
  }
21992
- const { supervised, agents } = describeState(state);
22408
+ const { supervised, agents, pools } = describeState(state);
21993
22409
  if (opts.json) {
21994
- console.log(JSON.stringify({ supervisorPid: state.supervisorPid, supervised, specPath: state.specPath, agents }, null, 2));
22410
+ console.log(JSON.stringify({ supervisorPid: state.supervisorPid, supervised, specPath: state.specPath, agents, pools }, null, 2));
21995
22411
  return;
21996
22412
  }
21997
22413
  console.log(`${supervised ? source_default.green("supervisor up") : source_default.red("supervisor down")} pid ${state.supervisorPid} ${source_default.dim(state.specPath)}`);
@@ -21999,6 +22415,10 @@ function nodeStatusCommand(opts) {
21999
22415
  const color = a.status === "running" ? source_default.green : a.status === "backoff" ? source_default.yellow : source_default.red;
22000
22416
  console.log(` ${a.name.padEnd(16)} ${color(a.status.padEnd(12))} pid ${String(a.pid ?? "-").padEnd(7)} restarts ${a.restarts}${a.lastExit ? source_default.dim(` last exit ${a.lastExit}`) : ""}`);
22001
22417
  }
22418
+ for (const p of pools) {
22419
+ const ready = p.ready === null ? "-" : String(p.ready);
22420
+ console.log(` pool ${p.name.padEnd(11)} running ${p.running}/${p.max} (min ${p.min}) ready ${ready} members ${p.members.join(", ")}${p.error ? source_default.red(` ${p.error}`) : ""}`);
22421
+ }
22002
22422
  }
22003
22423
 
22004
22424
  // src/index.ts
@@ -22018,7 +22438,7 @@ hook.command("cursor-stop").description("Cursor stop hook").option("--dir <path>
22018
22438
  hook.command("cursor-prompt").description("Cursor beforeSubmitPrompt hook").option("--dir <path>", "The agent's folder").action(hookCursorPromptCommand);
22019
22439
  program2.command("tasks").description("List tasks assigned to you (default: assigned + in_progress)").option("-a, --all", "Show all tasks in the project, not just yours").option("-s, --status <status>", "Filter by status (comma-separated: pending,assigned,in_progress,pending_verification,completed,blocked)").action(tasksCommand);
22020
22440
  var task = program2.command("task").description("Create or update a task");
22021
- task.command("create").description("Create a new task").option("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("-p, --priority <priority>", "low|normal|high|urgent").option("--to <agent>", "Assign to an agent (id or name)").option("--domains <list>", "Comma-separated domain tags").option("--specialization <s>", "Required specialization").option("-v, --verify <cmd>", "Shell command that must exit 0 to gate the `completed` transition (API must set ALLOW_SHELL_VERIFY=1)").option("--verify-cwd <path>", "Working directory for --verify (defaults to API server cwd)").option("--verify-kind <kind>", "Verifier kind: shell|file_exists|thread_concluded|reviewer_agent|human_approval").option("--verify-path <path>", "Path that must exist (--verify-kind file_exists)").option("--verify-thread <id>", "Thread that must be concluded (--verify-kind thread_concluded)").option("--verify-reviewer <agent>", "Agent (id or name) who must approve (--verify-kind reviewer_agent)").option("--review-by <agent>", "Shorthand for --verify-kind reviewer_agent --verify-reviewer <agent>").action(taskCreateCommand);
22441
+ task.command("create").description("Create a new task").option("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("-p, --priority <priority>", "low|normal|high|urgent").option("--to <agent>", "Assign to an agent (id or name)").option("--domains <list>", "Comma-separated domain tags").option("--specialization <s>", "Required specialization").option("-v, --verify <cmd>", "Shell command that must exit 0 to gate the `completed` transition (API must set ALLOW_SHELL_VERIFY=1)").option("--verify-cwd <path>", "Working directory for --verify (defaults to API server cwd)").option("--verify-kind <kind>", "Verifier kind: shell|file_exists|thread_concluded|reviewer_agent|human_approval|ci_passed").option("--verify-path <path>", "Path that must exist (--verify-kind file_exists) or commit sha (--verify-kind ci_passed)").option("--verify-thread <id>", "Thread that must be concluded (--verify-kind thread_concluded)").option("--verify-reviewer <agent>", "Agent (id or name) who must approve (--verify-kind reviewer_agent)").option("--review-by <agent>", "Shorthand for --verify-kind reviewer_agent --verify-reviewer <agent>").action(taskCreateCommand);
22022
22442
  task.command("start <id>").description("Mark a task as in_progress").option("-n, --note <note>", "Optional note to attach").action((id, options) => taskUpdateCommand(id, "in_progress", options));
22023
22443
  task.command("done <id>").description("Mark a task as completed").option("-n, --note <note>", "Optional note to attach").action((id, options) => taskUpdateCommand(id, "completed", options));
22024
22444
  task.command("block <id>").description("Mark a task as blocked").option("-n, --note <note>", "Describe what's blocking you").action((id, options) => taskUpdateCommand(id, "blocked", options));