@pitboss/cli 0.4.7 → 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";
@@ -20643,6 +20646,14 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
20643
20646
  human. If you need the answer before you can continue, block the task with
20644
20647
  \`blockedThreadId\` set to the thread the DM returned; you will be resumed when they answer.
20645
20648
 
20649
+ **A blocker travels up the chain.** When another agent's task blocks, the repo's orchestrator
20650
+ and the planner of that task's goal are told, as well as the owner. If that is you, read the
20651
+ question on its \`blockedThreadId\`. When the plan, the goal, its issue or its PR already answers
20652
+ it, reply there with the answer and where it comes from. As the planner, if the block shows the
20653
+ plan is wrong (a missing dependency, a task too big), fix the plan. Never answer for the owner on
20654
+ deletes, scope changes, approvals or publishing; those wait for the owner or their delegate.
20655
+ Otherwise do nothing: the owner has already been told.
20656
+
20646
20657
  2. Call \`mcp__relai__session_start\` for orientation. It is an **index, not an archive**: lists are
20647
20658
  capped and long bodies are clipped. Anything marked \`truncated: true\`, and anything named under
20648
20659
  \`notShown\`, is a pointer: fetch the real thing with the tool it names before you rely on it,
@@ -20659,7 +20670,8 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
20659
20670
  a one-line status on the task thread naming what you are checking) when you begin, then
20660
20671
  run the tests, compare the diff against the spec the task names, then
20661
20672
  \`mcp__relai__submit_review\` \u2014 \`approve\`, or \`reject\` with a specific note the author
20662
- 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.
20663
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.
20664
20676
 
20665
20677
  4. Call \`mcp__relai__get_my_tasks\` \u2014 if it returns no tasks, stop immediately.
@@ -20901,7 +20913,11 @@ Find the default branch with \`git remote show origin\`.
20901
20913
  \`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
20902
20914
  the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
20903
20915
  \`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
20904
- 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
20905
20921
  and independently testable so several agents can work at once. Use \`blockedBy\` only where
20906
20922
  one task truly needs another's result. Each description is self-contained: what to change,
20907
20923
  which files, how to test it, and the acceptance criteria.
@@ -20921,6 +20937,62 @@ Find the default branch with \`git remote show origin\`.
20921
20937
  Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
20922
20938
  the planning task's thread and complete it.
20923
20939
 
20940
+ **Review goals** (the order's \`kind\` is \`"review"\`; the planning task carries
20941
+ \`metadata.orderKind: "review"\`): the goal is one review posted on the existing pull request at the
20942
+ order's \`prUrl\`. There is no order branch, no draft PR and no merge, so skip steps 2 to 6 above.
20943
+ Nobody pushes to the PR's branch, approves it, or requests changes on it.
20944
+ 1. As the planner, read the PR with \`gh pr view <prUrl>\` and \`gh pr diff <prUrl>\`. Pick the
20945
+ angles it needs: correctness, security, simplicity, tests and consistency with the codebase for a
20946
+ real change; correctness and consistency alone for a small, low-risk one (docs, tests, under about
20947
+ 150 changed lines with no auth, input handling, secrets, money or migrations).
20948
+ 2. One task per angle, \`epicId\` = the order's \`epicId\`,
20949
+ \`metadata: { orderId, orderRole: "review", angle: "<angle>", prUrl }\`. Spread the angles
20950
+ across vendors: assign each to a different agent from \`mcp__relai__list_agents\`, preferring a
20951
+ different \`workerType\` from the agents already given one, and reuse agents only when there are
20952
+ more angles than agents. Each description names the angle, the PR, what that angle hunts for, and
20953
+ the reporting format below.
20954
+ 3. One lead task, \`blockedBy\` every angle task, assigned to yourself, with
20955
+ \`metadata: { orderId, orderRole: "review_lead", prUrl }\`.
20956
+ 4. When acceptance includes \`human_approval\`, add a gate task blocked by the lead
20957
+ (\`metadata: { orderId, orderRole: "gate" }\`, \`verifyKind: "human_approval"\`) and a post task
20958
+ blocked by the gate (\`metadata: { orderId, orderRole: "post_review", prUrl }\`). Without it,
20959
+ the lead posts the review itself.
20960
+ 5. If the order has \`relatedRepos\`, copy them into every angle and lead task's metadata and name them in
20961
+ each description, so reviewers check the contracts this PR depends on in those repos.
20962
+ 6. Post the task list on the planning task's thread and complete it.
20963
+
20964
+ **orderRole "review"** (one angle of a review goal):
20965
+ - Read the diff and as much surrounding code as you need. To run anything, check out the PR head
20966
+ detached: \`git fetch origin pull/<n>/head && git checkout --detach FETCH_HEAD\`. Never push, never
20967
+ comment on the PR, never edit the repo's history.
20968
+ - Verify by running code where you can. Report your 3 to 5 best-evidenced findings for your angle, most
20969
+ severe first, each with severity (blocker, high, medium, low), file:line, and a concrete failing
20970
+ input or scenario. "No findings" is a valid report. A missing code comment is not a finding.
20971
+ - When the task names \`relatedRepos\`, read them for any contract this change depends on (an endpoint it
20972
+ calls, a helper it imports, a config value it reads). Read only, outside your worktree: a shallow
20973
+ clone into a temp directory (\`gh repo clone <url> <tmp dir> -- --depth 1\`) or one file at a time with
20974
+ \`gh api repos/<owner>/<repo>/contents/<path>\`. Never push, branch, comment or open anything there.
20975
+ A finding that rests on a related repo cites the file and line there. If you could not read the
20976
+ repo, mark that finding UNPROVEN and say why rather than guessing what the other side does.
20977
+ - Post the report with \`mcp__relai__add_task_comment\` on this task, then complete it.
20978
+
20979
+ **orderRole "review_lead"**:
20980
+ - Read every angle task's report with \`mcp__relai__get_task_comments\`. Merge duplicates, drop
20981
+ findings you cannot reproduce or whose premise is wrong, and rank what is left. Keep an UNPROVEN mark
20982
+ on any finding about a related repo nobody could read. Severity sets the
20983
+ verdict: any blocker or high means changes are needed; otherwise it is safe to merge with the
20984
+ noted follow-ups.
20985
+ - Write one review: the verdict in the first line, then the findings, each with file:line, the
20986
+ failure and the fix you would ask for, then which angles ran, each with its agent's name and vendor
20987
+ (\`workerType\` from \`mcp__relai__list_agents\`), and which related repos were read. Keep it short.
20988
+ - Without a \`human_approval\` gate, post it with \`gh pr review <prUrl> --comment --body-file <file>\`
20989
+ and put the review URL on the task's thread. With a gate, post the review as a comment on this task
20990
+ for the owner to approve, and do not touch the PR. Then complete.
20991
+
20992
+ **orderRole "post_review"**: once the gate is approved, post the lead's review comment from the
20993
+ lead task's thread with \`gh pr review <prUrl> --comment --body-file <file>\`, put the review URL on
20994
+ this task's thread, and complete.
20995
+
20924
20996
  **orderRole "plan_review"** (assigned to the planner, reviewed by another agent):
20925
20997
  - After the task graph is written, complete each plan review task. That hands the plan to its
20926
20998
  reviewer. Every build task waits until all plan reviews are approved.
@@ -21106,6 +21178,18 @@ async function recordSessionUsage(conn, startIds, usage, logPrefix) {
21106
21178
  await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
21107
21179
  }
21108
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
+
21109
21193
  // ../claude-worker/src/session.ts
21110
21194
  function mcpServerEntry() {
21111
21195
  return fileURLToPath2(new URL("../../mcp-server/src/index.ts", import.meta.url));
@@ -21143,7 +21227,7 @@ async function runClaudeSession(config) {
21143
21227
  const mcpConfigPath = writeMcpConfig(config);
21144
21228
  const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
21145
21229
  try {
21146
- return await new Promise((resolve8, reject) => {
21230
+ return await new Promise((resolve9, reject) => {
21147
21231
  const args = [
21148
21232
  "--print",
21149
21233
  "--mcp-config",
@@ -21154,7 +21238,7 @@ async function runClaudeSession(config) {
21154
21238
  "stream-json",
21155
21239
  "--verbose",
21156
21240
  "--model",
21157
- config.model,
21241
+ pinnedModel() ?? config.model,
21158
21242
  "--add-dir",
21159
21243
  config.repoPath
21160
21244
  ];
@@ -21223,7 +21307,7 @@ async function runClaudeSession(config) {
21223
21307
  const detail = (resultError || stderrOutput || "").trim();
21224
21308
  reject(new Error(`claude exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21225
21309
  } else {
21226
- resolve8(unique);
21310
+ resolve9(unique);
21227
21311
  }
21228
21312
  };
21229
21313
  finish.then(settle, settle);
@@ -21275,8 +21359,8 @@ function classifySessionError(text) {
21275
21359
 
21276
21360
  // ../claude-worker/src/adapters/cursor.ts
21277
21361
  var bin = () => process.env.CURSOR_BIN ?? "cursor-agent";
21278
- function cursorArgs(repoPath, prompt) {
21279
- 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];
21280
21364
  }
21281
21365
  function readCursorEvent(line) {
21282
21366
  let event;
@@ -21300,10 +21384,10 @@ function readCursorEvent(line) {
21300
21384
  return {};
21301
21385
  }
21302
21386
  async function runCursorSession(config) {
21303
- const args = cursorArgs(config.repoPath, buildPrompt(config));
21387
+ const args = cursorArgs(config.repoPath, buildPrompt(config), pinnedModel());
21304
21388
  console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
21305
21389
  const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
21306
- return new Promise((resolve8, reject) => {
21390
+ return new Promise((resolve9, reject) => {
21307
21391
  const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21308
21392
  console.log(`[cursor-worker] Process PID: ${proc.pid}`);
21309
21393
  const toolsUsed = [];
@@ -21342,7 +21426,7 @@ async function runCursorSession(config) {
21342
21426
  const detail = (resultError || stderrOutput).trim();
21343
21427
  reject(new Error(`cursor-agent exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21344
21428
  } else {
21345
- resolve8([...new Set(toolsUsed)]);
21429
+ resolve9([...new Set(toolsUsed)]);
21346
21430
  }
21347
21431
  };
21348
21432
  finish.then(settle, settle);
@@ -21381,10 +21465,11 @@ import { unlinkSync as unlinkSync2 } from "fs";
21381
21465
  var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
21382
21466
  var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
21383
21467
  var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
21384
- function copilotArgs(repoPath, prompt, mcpConfigPath) {
21468
+ function copilotArgs(repoPath, prompt, mcpConfigPath, model) {
21385
21469
  return [
21386
21470
  "-p",
21387
21471
  prompt,
21472
+ ...model ? ["--model", model] : [],
21388
21473
  "--allow-all-tools",
21389
21474
  "--output-format",
21390
21475
  "json",
@@ -21414,9 +21499,9 @@ function readCopilotEvent(line) {
21414
21499
  }
21415
21500
  async function runCopilotSession(config) {
21416
21501
  const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
21417
- const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
21502
+ const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath, pinnedModel());
21418
21503
  console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
21419
- return new Promise((resolve8, reject) => {
21504
+ return new Promise((resolve9, reject) => {
21420
21505
  const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21421
21506
  console.log(`[copilot-worker] Process PID: ${proc.pid}`);
21422
21507
  const toolsUsed = [];
@@ -21453,7 +21538,7 @@ async function runCopilotSession(config) {
21453
21538
  const detail = (sessionError || stderrOutput).trim();
21454
21539
  reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21455
21540
  } else {
21456
- resolve8([...new Set(toolsUsed)]);
21541
+ resolve9([...new Set(toolsUsed)]);
21457
21542
  }
21458
21543
  });
21459
21544
  proc.on("error", (err) => {
@@ -21487,7 +21572,7 @@ var bin3 = () => process.env.CODEX_BIN ?? "codex";
21487
21572
  var codexHome = () => process.env.CODEX_HOME ?? join14(homedir5(), ".codex");
21488
21573
  var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
21489
21574
  var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
21490
- function codexArgs(repoPath, prompt, profile) {
21575
+ function codexArgs(repoPath, prompt, profile, model) {
21491
21576
  return [
21492
21577
  "exec",
21493
21578
  "--json",
@@ -21498,6 +21583,7 @@ function codexArgs(repoPath, prompt, profile) {
21498
21583
  repoPath,
21499
21584
  "-p",
21500
21585
  profile,
21586
+ ...model ? ["-m", model] : [],
21501
21587
  prompt
21502
21588
  ];
21503
21589
  }
@@ -21546,9 +21632,9 @@ async function runCodexSession(config) {
21546
21632
  } catch {
21547
21633
  }
21548
21634
  };
21549
- const args = codexArgs(config.repoPath, buildPrompt(config), profile);
21635
+ const args = codexArgs(config.repoPath, buildPrompt(config), profile, pinnedModel());
21550
21636
  console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
21551
- return new Promise((resolve8, reject) => {
21637
+ return new Promise((resolve9, reject) => {
21552
21638
  const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21553
21639
  console.log(`[codex-worker] Process PID: ${proc.pid}`);
21554
21640
  const toolsUsed = [];
@@ -21582,7 +21668,7 @@ async function runCodexSession(config) {
21582
21668
  const detail = (sessionError || stderrOutput).trim();
21583
21669
  reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21584
21670
  } else {
21585
- resolve8([...new Set(toolsUsed)]);
21671
+ resolve9([...new Set(toolsUsed)]);
21586
21672
  }
21587
21673
  });
21588
21674
  proc.on("error", (err) => {
@@ -21619,7 +21705,65 @@ var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
21619
21705
  // ../claude-worker/src/config.ts
21620
21706
  var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
21621
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
+
21622
21765
  // src/node/spec.ts
21766
+ import { join as join16, resolve as resolve5, sep as sep2 } from "node:path";
21623
21767
  var DEFAULT_API2 = "https://api.pitboss.dev";
21624
21768
  var MAX_POOL_SIZE = 32;
21625
21769
  var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
@@ -21631,7 +21775,14 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
21631
21775
  function poolMembers(pool) {
21632
21776
  return Array.from({ length: pool.max }, (_, i) => {
21633
21777
  const name = `${pool.pool}-${i + 1}`;
21634
- 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
+ };
21635
21786
  });
21636
21787
  }
21637
21788
  var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
@@ -21648,6 +21799,18 @@ function specializationOf(a, i) {
21648
21799
  }
21649
21800
  return specialization;
21650
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
+ }
21651
21814
  function parsePool(a, i, specDir) {
21652
21815
  if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
21653
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)`);
@@ -21663,10 +21826,10 @@ function parsePool(a, i, specDir) {
21663
21826
  if (typeof min !== "number" || !Number.isInteger(min) || min < 0 || min > max) {
21664
21827
  throw new Error(`agents[${i}].min must be an integer from 0 to max`);
21665
21828
  }
21666
- const base2 = resolve4(specDir, a.base);
21667
- const worktreeRoot = resolve4(specDir, a.worktreeRoot);
21829
+ const base2 = resolve5(specDir, a.base);
21830
+ const worktreeRoot = resolve5(specDir, a.worktreeRoot);
21668
21831
  if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
21669
- 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 };
21670
21833
  }
21671
21834
  function parseSpec(text, specDir) {
21672
21835
  let raw;
@@ -21692,7 +21855,8 @@ function parseSpec(text, specDir) {
21692
21855
  if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
21693
21856
  if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
21694
21857
  const runtime = runtimeOf(a, i);
21695
- 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) });
21696
21860
  });
21697
21861
  for (const p of pools) {
21698
21862
  for (const a of agents) {
@@ -21719,13 +21883,15 @@ function parseSpec(text, specDir) {
21719
21883
  }
21720
21884
 
21721
21885
  // src/node/store.ts
21722
- 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";
21723
- 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";
21724
21888
  import { randomBytes as randomBytes4 } from "node:crypto";
21725
- var nodeDir = () => join16(configDir(), "node");
21726
- var credsPath = (name) => join16(nodeDir(), "agents", `${name}.json`);
21727
- var statePath2 = () => join16(nodeDir(), "state.json");
21728
- 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);
21729
21895
  function writePrivate(path3, value) {
21730
21896
  mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
21731
21897
  const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
@@ -21734,7 +21900,7 @@ function writePrivate(path3, value) {
21734
21900
  }
21735
21901
  function readJson(path3) {
21736
21902
  if (!existsSync8(path3)) return null;
21737
- return JSON.parse(readFileSync11(path3, "utf-8"));
21903
+ return JSON.parse(readFileSync12(path3, "utf-8"));
21738
21904
  }
21739
21905
  var readCreds = (name) => readJson(credsPath(name));
21740
21906
  var writeCreds = (name, creds) => writePrivate(credsPath(name), creds);
@@ -21743,14 +21909,187 @@ var removeCreds = (name) => {
21743
21909
  if (existsSync8(path3)) rmSync4(path3);
21744
21910
  };
21745
21911
  var listStoredAgentNames = () => {
21746
- const dir = join16(nodeDir(), "agents");
21912
+ const dir = join17(nodeDir(), "agents");
21747
21913
  if (!existsSync8(dir)) return [];
21748
21914
  return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
21749
21915
  };
21750
21916
  var readState = () => readJson(statePath2());
21751
21917
  var writeState = (state) => writePrivate(statePath2(), state);
21752
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
+
21753
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
+ }
21754
22093
  function collectInvites(agentNames, flags, env2) {
21755
22094
  const byLower = new Map(agentNames.map((n) => [n.toLowerCase(), n]));
21756
22095
  const out = /* @__PURE__ */ new Map();
@@ -21787,6 +22126,7 @@ function matchInvites(agents, invites) {
21787
22126
  }
21788
22127
  return { matched, ungranted, extra: left, refused };
21789
22128
  }
22129
+ var MAX_REQUESTED = 32;
21790
22130
  function groupByRepo(agents, originOf2) {
21791
22131
  const groups = /* @__PURE__ */ new Map();
21792
22132
  for (const agent of agents) {
@@ -21794,7 +22134,10 @@ function groupByRepo(agents, originOf2) {
21794
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`);
21795
22135
  groups.set(remote, [...groups.get(remote) ?? [], agent]);
21796
22136
  }
21797
- 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
+ })));
21798
22141
  }
21799
22142
  var originOf = (worktree) => git(["remote", "get-url", "origin"], worktree);
21800
22143
  async function abandonMinted(spec, agent, minted, reason) {
@@ -21900,10 +22243,12 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
21900
22243
  console.log(source_default.bold(`
21901
22244
  Approve ${repoName}`));
21902
22245
  console.log(` On the approval screen add these agents as workers:`);
21903
- 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
+ }
21904
22249
  let grant;
21905
22250
  try {
21906
- 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");
21907
22252
  } catch (err) {
21908
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.`));
21909
22254
  continue;
@@ -21943,6 +22288,106 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
21943
22288
  return have;
21944
22289
  }
21945
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
+
21946
22391
  // src/node/supervisor.ts
21947
22392
  function nextDelay(consecutiveFailures, { baseMs, maxMs }) {
21948
22393
  return Math.min(baseMs * 2 ** Math.max(0, consecutiveFailures - 1), maxMs);
@@ -22067,44 +22512,67 @@ var Supervisor = class {
22067
22512
  }
22068
22513
  };
22069
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
+
22070
22538
  // src/node/entries.ts
22071
- import { existsSync as existsSync9 } from "node:fs";
22072
- 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";
22073
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.";
22074
22542
  function checkoutWorker(dir) {
22075
22543
  return {
22076
- command: join17(dir, "node_modules", ".bin", "tsx"),
22077
- args: [join17(dir, "src", "index.ts")]
22544
+ command: join19(dir, "node_modules", ".bin", "tsx"),
22545
+ args: [join19(dir, "src", "index.ts")]
22078
22546
  };
22079
22547
  }
22080
22548
  function checkoutMcp(dir, execPath) {
22081
22549
  return {
22082
22550
  command: execPath,
22083
22551
  args: [
22084
- join17(dir, "node_modules", "tsx", "dist", "cli.mjs"),
22085
- join17(dir, "src", "index.ts")
22552
+ join19(dir, "node_modules", "tsx", "dist", "cli.mjs"),
22553
+ join19(dir, "src", "index.ts")
22086
22554
  ]
22087
22555
  };
22088
22556
  }
22089
22557
  function walkFor(here, packageName, exists) {
22090
- for (let dir = dirname9(here); dirname9(dir) !== dir; dir = dirname9(dir)) {
22091
- const candidate = join17(dir, packageName);
22092
- 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;
22093
22561
  }
22094
22562
  return null;
22095
22563
  }
22096
22564
  function resolveEventWorker(opts) {
22097
22565
  const env2 = opts.env ?? process.env;
22098
22566
  const execPath = opts.execPath ?? process.execPath;
22099
- const exists = opts.exists ?? existsSync9;
22567
+ const exists = opts.exists ?? existsSync11;
22100
22568
  const override = env2.PITBOSS_EVENT_WORKER;
22101
22569
  if (override) {
22102
- const resolved = resolve5(override);
22570
+ const resolved = resolve7(override);
22103
22571
  if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
22104
- if (exists(join17(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
22572
+ if (exists(join19(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
22105
22573
  throw new Error(MISSING_WORKER);
22106
22574
  }
22107
- const bundled = join17(dirname9(opts.here), "event-worker.js");
22575
+ const bundled = join19(dirname11(opts.here), "event-worker.js");
22108
22576
  if (exists(bundled)) return { command: execPath, args: [bundled] };
22109
22577
  const dir = walkFor(opts.here, "event-worker", exists);
22110
22578
  if (dir) return checkoutWorker(dir);
@@ -22112,8 +22580,8 @@ function resolveEventWorker(opts) {
22112
22580
  }
22113
22581
  function resolveMcpServer(opts) {
22114
22582
  const execPath = opts.execPath ?? process.execPath;
22115
- const exists = opts.exists ?? existsSync9;
22116
- const bundled = join17(dirname9(opts.here), "mcp.js");
22583
+ const exists = opts.exists ?? existsSync11;
22584
+ const bundled = join19(dirname11(opts.here), "mcp.js");
22117
22585
  if (exists(bundled)) return { command: execPath, args: [bundled] };
22118
22586
  const dir = walkFor(opts.here, "mcp-server", exists);
22119
22587
  if (dir) return checkoutMcp(dir, execPath);
@@ -22221,176 +22689,41 @@ function loadPruneTargets(names) {
22221
22689
  });
22222
22690
  }
22223
22691
 
22224
- // src/node/pool.ts
22225
- import { execFileSync as execFileSync6 } from "node:child_process";
22226
- import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as mkdirSync8, realpathSync as realpathSync4 } from "node:fs";
22227
- import { basename as basename2, dirname as dirname10, join as join18, resolve as resolve6 } from "node:path";
22228
- var IDLE_STOP_MS = 10 * 6e4;
22229
- var RECONCILE_MS = 15e3;
22230
- var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
22231
- var REMARK_AFTER_MS = 24 * 60 * 6e4;
22232
- function positiveMs(raw, fallback2) {
22233
- const n = Number(raw);
22234
- return raw && Number.isInteger(n) && n > 0 ? n : fallback2;
22235
- }
22236
- function poolTimings(env2 = process.env) {
22237
- return {
22238
- reconcileMs: positiveMs(env2.PITBOSS_POOL_RECONCILE_MS, RECONCILE_MS),
22239
- idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
22240
- };
22241
- }
22242
- function effectiveBounds(spec, desired) {
22243
- if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
22244
- const max = Math.min(desired.max, spec.max);
22245
- return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
22246
- }
22247
- function reconcilePool({ pending, members, min, max, idleStopMs }) {
22248
- const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
22249
- const running = members.filter((m) => m.running);
22250
- const stopped = members.filter((m) => !m.running);
22251
- let count = running.length;
22252
- const start = [];
22253
- const take = (m) => {
22254
- start.push(m.name);
22255
- count++;
22256
- };
22257
- for (const m of stopped) {
22258
- if (count >= max) break;
22259
- if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
22260
- }
22261
- const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
22262
- let demand = pending - running.filter(isIdle).length;
22263
- for (const m of spare()) {
22264
- if (demand <= 0 || count >= max) break;
22265
- take(m);
22266
- demand--;
22267
- }
22268
- for (const m of spare()) {
22269
- if (count >= min || count >= max) break;
22270
- take(m);
22271
- }
22272
- if (start.length > 0) return { start, stop: [] };
22273
- const stop = [];
22274
- let surplus = running.filter(isIdle).length - pending;
22275
- const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
22276
- for (const m of eligible) {
22277
- if (surplus <= 0 || count <= min) break;
22278
- stop.push(m.name);
22279
- count--;
22280
- surplus--;
22281
- }
22282
- return { start, stop };
22283
- }
22284
- function summarizeTasks(ready, held, members) {
22285
- const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
22286
- const counts = Object.fromEntries(
22287
- members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
22288
- );
22289
- const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
22290
- let readyAssigned = 0;
22291
- for (const t of ready) {
22292
- const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
22293
- if (!name) continue;
22294
- counts[name].readyAssigned++;
22295
- readyAssigned++;
22296
- }
22297
- for (const t of held) {
22298
- const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
22299
- if (!name) continue;
22300
- if (t.status === "assigned") counts[name].assigned++;
22301
- if (t.status === "in_progress") counts[name].inProgress++;
22302
- }
22303
- return { pending, ready: pending + readyAssigned, members: counts };
22304
- }
22305
- function poolRootProblem(root, protectedPaths) {
22306
- for (const p of protectedPaths) {
22307
- if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
22308
- }
22309
- return null;
22310
- }
22311
- function memberPathProblem(s) {
22312
- if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
22313
- if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
22314
- if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
22315
- return null;
22316
- }
22317
- function realpathAllowMissing(path3) {
22318
- const abs = resolve6(path3);
22319
- if (existsSync10(abs)) return realpathSync4.native(abs);
22320
- return join18(realpathAllowMissing(dirname10(abs)), basename2(abs));
22321
- }
22322
- function run2(cwd, args) {
22323
- try {
22324
- return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
22325
- } catch (err) {
22326
- const stderr = err.stderr?.toString().trim();
22327
- throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
22328
- }
22329
- }
22330
- var commonDir = (cwd) => {
22331
- const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
22332
- return dir ? realpathSync4.native(dir) : null;
22333
- };
22334
- function defaultBranch(cwd) {
22335
- const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
22336
- let ref = head();
22337
- if (!ref) {
22338
- git(["remote", "set-head", "origin", "--auto"], cwd);
22339
- ref = head();
22340
- }
22341
- if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
22342
- return ref.slice("origin/".length);
22343
- }
22344
- function ensurePoolWorktrees(pool, otherWorktrees = []) {
22345
- if (!existsSync10(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
22346
- const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
22347
- const baseCommon = commonDir(pool.base);
22348
- if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
22349
- const root = realpathAllowMissing(pool.worktreeRoot);
22350
- const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
22351
- if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
22352
- const members = poolMembers({ ...pool, worktreeRoot: root });
22353
- for (const m of members) {
22354
- let symlink = false;
22355
- try {
22356
- symlink = lstatSync3(m.worktree).isSymbolicLink();
22357
- } catch {
22358
- symlink = false;
22359
- }
22360
- const exists = !symlink && existsSync10(m.worktree);
22361
- const problem = memberPathProblem({
22362
- path: m.worktree,
22363
- symlink,
22364
- exists,
22365
- baseCommonDir: baseCommon,
22366
- commonDir: exists ? commonDir(m.worktree) : null,
22367
- tracked: !exists && !symlink && isTracked(m.worktree)
22368
- });
22369
- if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
22370
- }
22371
- const missing = members.filter((m) => !existsSync10(m.worktree));
22372
- if (missing.length > 0) {
22373
- mkdirSync8(root, { recursive: true });
22374
- run2(pool.base, ["fetch", "-q", "origin"]);
22375
- const branch = defaultBranch(pool.base);
22376
- run2(pool.base, ["worktree", "prune"]);
22377
- for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
22378
- }
22379
- return members.map((m) => realpathSync4.native(m.worktree));
22380
- }
22381
- function resetWorktree(worktree) {
22382
- run2(worktree, ["fetch", "-q", "--prune", "origin"]);
22383
- const target = `origin/${defaultBranch(worktree)}`;
22384
- run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
22385
- run2(worktree, ["reset", "-q", "--hard", target]);
22386
- 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;
22387
22720
  }
22388
22721
 
22389
22722
  // src/commands/node.ts
22390
22723
  var HERE = fileURLToPath3(import.meta.url);
22391
22724
  function inspectProcess(pid) {
22392
22725
  try {
22393
- 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();
22394
22727
  const match = /^(\d+)\s+(.*)$/.exec(out);
22395
22728
  return match ? { pgid: Number(match[1]), command: match[2] } : null;
22396
22729
  } catch {
@@ -22414,7 +22747,9 @@ function childSpec(agent, creds, worker, onDemand = false) {
22414
22747
  REPO_ID: creds.repoId,
22415
22748
  REPO_PATH: agent.worktree,
22416
22749
  SPECIALIZATION: agent.specialization,
22417
- WORKER_RUNTIME: agent.runtime
22750
+ WORKER_RUNTIME: agent.runtime,
22751
+ PITBOSS_ACTIVITY_FILE: activityPath(agent.name),
22752
+ PITBOSS_MODEL_FILE: resolve8(modelPath(agent.name))
22418
22753
  }
22419
22754
  };
22420
22755
  }
@@ -22430,12 +22765,13 @@ function prepareAgent(agent, creds, mcp) {
22430
22765
  excludeIfUntracked(root, target);
22431
22766
  },
22432
22767
  run(bin4, args, cwd) {
22433
- execFileSync7(bin4, args, { cwd, stdio: "ignore" });
22768
+ execFileSync8(bin4, args, { cwd, stdio: "ignore" });
22434
22769
  }
22435
22770
  });
22436
22771
  }
22437
22772
  function spawnWorker(spec) {
22438
- mkdirSync9(dirname11(spec.logPath), { recursive: true, mode: 448 });
22773
+ mkdirSync11(dirname12(spec.logPath), { recursive: true, mode: 448 });
22774
+ rmSync6(activityPath(spec.name), { force: true });
22439
22775
  const log = openSync(spec.logPath, "a", 384);
22440
22776
  try {
22441
22777
  const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
@@ -22482,6 +22818,63 @@ async function reportScale(creds, event) {
22482
22818
  });
22483
22819
  if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
22484
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
+ }
22485
22878
  function startPoolLoop(run3, sup, record) {
22486
22879
  const { pool, members } = run3;
22487
22880
  const { reconcileMs, idleStopMs } = poolTimings();
@@ -22567,19 +22960,20 @@ function startPoolLoop(run3, sup, record) {
22567
22960
  const timer = setInterval(() => void tick(), reconcileMs);
22568
22961
  return () => clearInterval(timer);
22569
22962
  }
22963
+ var MODEL_REFRESH_MS = 15e3;
22570
22964
  async function nodeUpCommand(specFile, opts = {}) {
22571
22965
  try {
22572
- const specPath = resolve7(specFile);
22573
- const specRealpath = realpathSync5.native(specPath);
22574
- 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));
22575
22969
  const previous = readState();
22576
22970
  if (previous && previous.supervisorPid !== process.pid && isAlive(previous.supervisorPid)) {
22577
22971
  throw new Error(`a node is already running (pid ${previous.supervisorPid}, ${previous.specPath}). Stop it first.`);
22578
22972
  }
22579
22973
  for (const a of parsed.agents) {
22580
- 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`);
22581
22975
  }
22582
- 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) }));
22583
22977
  const pools = parsed.pools.map((pool) => {
22584
22978
  const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
22585
22979
  return { pool, members: poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] })) };
@@ -22609,6 +23003,20 @@ async function nodeUpCommand(specFile, opts = {}) {
22609
23003
  const inviteCodes = collectInvites(spec.agents.map((a) => a.name), opts.invite ?? [], process.env);
22610
23004
  const creds = await ensureIdentities(spec, inviteCodes, specRealpath);
22611
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();
22612
23020
  if (runnable.length === 0) throw new Error("no agent has credentials, nothing to start");
22613
23021
  const mcp = resolveMcpServer({ here: HERE });
22614
23022
  for (const a of runnable) prepareAgent(a, creds.get(a.name), mcp);
@@ -22647,6 +23055,12 @@ async function nodeUpCommand(specFile, opts = {}) {
22647
23055
  process.on("SIGTERM", shutdown);
22648
23056
  process.on("SIGHUP", shutdown);
22649
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));
22650
23064
  for (const { pool, members } of pools) {
22651
23065
  const ready = members.filter((m) => creds.has(m.name)).map((agent) => ({ agent, creds: creds.get(agent.name) }));
22652
23066
  if (ready.length === 0) {
@@ -22667,9 +23081,9 @@ async function nodeUpCommand(specFile, opts = {}) {
22667
23081
  }
22668
23082
  async function nodePruneCommand(specFile, opts = {}) {
22669
23083
  try {
22670
- const specPath = resolve7(specFile);
22671
- const specRealpath = realpathSync5.native(specPath);
22672
- 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));
22673
23087
  const { orphans: names, unattributed } = classifyStored(allAgents(spec).map((a) => a.name), specRealpath);
22674
23088
  if (names.length === 0) {
22675
23089
  console.log("No stored credentials are missing from the spec.");
@@ -22775,6 +23189,7 @@ var token = program2.command("token").description("Manage your agent's API token
22775
23189
  token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
22776
23190
  token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
22777
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);
22778
23193
  var node = program2.command("node").description("Run this machine's agents headless and keep them running");
22779
23194
  node.command("up <spec>").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them)").option(
22780
23195
  "--invite <name=code>",