@shipster/cli 0.5.0 → 0.5.1

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
@@ -18509,7 +18509,7 @@ function readableFrom(current, legacy, overridden) {
18509
18509
  if (overridden || existsSync(current) || !existsSync(legacy)) return current;
18510
18510
  if (!warnedFor.has(legacy)) {
18511
18511
  warnedFor.add(legacy);
18512
- console.error(`pitboss: reading ${legacy}; the next write moves it to ${current}.`);
18512
+ console.error(`shipster: reading ${legacy}; the next write moves it to ${current}.`);
18513
18513
  }
18514
18514
  return legacy;
18515
18515
  }
@@ -18523,7 +18523,7 @@ function retireLegacy(legacy, current, overridden) {
18523
18523
  rmSync2(legacy);
18524
18524
  } catch (err) {
18525
18525
  if (err.code === "ENOENT") return;
18526
- console.error(`pitboss: could not remove ${legacy} (${String(err)}); it may still hold a token, delete it by hand.`);
18526
+ console.error(`shipster: could not remove ${legacy} (${String(err)}); it may still hold a token, delete it by hand.`);
18527
18527
  }
18528
18528
  }
18529
18529
  function configFileInUse() {
@@ -18579,7 +18579,7 @@ function writeConfig(config) {
18579
18579
  function requireConfig() {
18580
18580
  const config = readConfig();
18581
18581
  if (!config) {
18582
- console.error("Not initialized. Run `pitboss init` first.");
18582
+ console.error("Not initialized. Run `shipster init` first.");
18583
18583
  process.exit(1);
18584
18584
  }
18585
18585
  return config;
@@ -19069,7 +19069,7 @@ async function repoContextShowCommand() {
19069
19069
  try {
19070
19070
  const project = await client.getRepo(config.repoId);
19071
19071
  if (!project.context) {
19072
- console.log(source_default.dim("(no context set \u2014 use `pitboss repo context edit` to add some)"));
19072
+ console.log(source_default.dim("(no context set \u2014 use `shipster repo context edit` to add some)"));
19073
19073
  return;
19074
19074
  }
19075
19075
  console.log(project.context);
@@ -19174,7 +19174,7 @@ async function threadsCommand() {
19174
19174
  const threads = await client.listThreads(config.repoId);
19175
19175
  spinner.stop();
19176
19176
  if (threads.length === 0) {
19177
- console.log(source_default.dim("No threads yet. Create one with `pitboss thread new <title>`."));
19177
+ console.log(source_default.dim("No threads yet. Create one with `shipster thread new <title>`."));
19178
19178
  return;
19179
19179
  }
19180
19180
  console.log();
@@ -19296,7 +19296,7 @@ async function inboxCommand(options) {
19296
19296
  console.log(` ${source_default.blue("proposed")} ${source_default.dim(task2.id)}`);
19297
19297
  console.log(` ${source_default.dim("suggests:")} ${suggested ?? "\u2014"} ${source_default.dim(date)}`);
19298
19298
  console.log(` ${titlePreview}`);
19299
- console.log(` ${source_default.dim(`pitboss task commit ${task2.id} --to <agent|@auto> (or --reject)`)}`);
19299
+ console.log(` ${source_default.dim(`shipster task commit ${task2.id} --to <agent|@auto> (or --reject)`)}`);
19300
19300
  console.log();
19301
19301
  }
19302
19302
  }
@@ -19309,7 +19309,7 @@ async function inboxCommand(options) {
19309
19309
  console.log(` ${source_default.yellow("review")} ${source_default.dim(task2.id)}`);
19310
19310
  console.log(` ${source_default.dim("from:")} ${task2.assignedTo ?? "?"} ${source_default.dim(date)}`);
19311
19311
  console.log(` ${titlePreview}`);
19312
- console.log(` ${source_default.dim(`pitboss task review ${task2.id} --decision approve|reject`)}`);
19312
+ console.log(` ${source_default.dim(`shipster task review ${task2.id} --decision approve|reject`)}`);
19313
19313
  console.log();
19314
19314
  }
19315
19315
  }
@@ -19381,7 +19381,7 @@ async function statusCommand() {
19381
19381
  }
19382
19382
  if (unreadTotal > 0) {
19383
19383
  console.log();
19384
- console.log(source_default.yellow(` ${unreadTotal} unread message${unreadTotal === 1 ? "" : "s"}: run \`pitboss inbox\``));
19384
+ console.log(source_default.yellow(` ${unreadTotal} unread message${unreadTotal === 1 ? "" : "s"}: run \`shipster inbox\``));
19385
19385
  }
19386
19386
  } catch (err) {
19387
19387
  spinner.fail(source_default.red("Failed to fetch status"));
@@ -19446,7 +19446,7 @@ async function startCommand() {
19446
19446
  }
19447
19447
  const totalUnread = session.unreadCount ?? session.unreadMessages.length;
19448
19448
  if (totalUnread > 5) {
19449
- console.log(source_default.dim(` \u2026and ${totalUnread - 5} more \u2014 run \`pitboss inbox\``));
19449
+ console.log(source_default.dim(` \u2026and ${totalUnread - 5} more \u2014 run \`shipster inbox\``));
19450
19450
  }
19451
19451
  }
19452
19452
  console.log();
@@ -19835,7 +19835,7 @@ ${live.length} live, ${rows.length - live.length} revoked.`);
19835
19835
  console.log(source_default.yellow("Cannot tell which of these you are holding: this config authenticates with a shared secret, not an agent token."));
19836
19836
  }
19837
19837
  if (live.length > 1) {
19838
- console.log(source_default.yellow(`${live.length} live tokens: every one of them authenticates. 'pitboss token rotate' collapses them to one.`));
19838
+ console.log(source_default.yellow(`${live.length} live tokens: every one of them authenticates. 'shipster token rotate' collapses them to one.`));
19839
19839
  }
19840
19840
  const dormant = live.filter((r) => !r.lastUsedAt && r.current !== true);
19841
19841
  if (dormant.length) {
@@ -20028,7 +20028,7 @@ async function repoInviteCommand(opts) {
20028
20028
  console.log(`
20029
20029
  ${source_default.bold("Send this to the new agent:")}
20030
20030
 
20031
- ${source_default.cyan(`pitboss login --invite ${code}`)}
20031
+ ${source_default.cyan(`shipster login --invite ${code}`)}
20032
20032
 
20033
20033
  ${source_default.dim("expires:")} ${invite.expiresAt}
20034
20034
  ${source_default.dim("invite id:")} ${invite.id}
@@ -20080,8 +20080,8 @@ async function loginCommand(opts) {
20080
20080
  process.exit(1);
20081
20081
  }
20082
20082
  if (!opts.invite && !opts.token) {
20083
- console.error(source_default.red("pitboss login requires --invite <code> or --token <token>"));
20084
- console.error(source_default.dim("Ask a repo member to run `pitboss repo invite` and share the code, or create an agent in the cloud dashboard."));
20083
+ console.error(source_default.red("shipster login requires --invite <code> or --token <token>"));
20084
+ console.error(source_default.dim("Ask a repo member to run `shipster repo invite` and share the code, or create an agent in the cloud dashboard."));
20085
20085
  process.exit(1);
20086
20086
  }
20087
20087
  if (opts.workerType !== void 0) {
@@ -20174,7 +20174,7 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
20174
20174
  failAfterRedeem(creds, "Not in a git repo. Clone first:", [
20175
20175
  ` git clone ${repoUrl}`,
20176
20176
  ` cd ${repoName}`,
20177
- ` pitboss login \u2026`
20177
+ ` shipster login \u2026`
20178
20178
  ]);
20179
20179
  }
20180
20180
  const origin = getOriginUrl(gitRoot3);
@@ -20205,7 +20205,7 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
20205
20205
  "Use a separate worktree for this agent:",
20206
20206
  ` git worktree add ../${repoName}-${slug}`,
20207
20207
  ` cd ../${repoName}-${slug}`,
20208
- ` pitboss login \u2026`
20208
+ ` shipster login \u2026`
20209
20209
  ]
20210
20210
  );
20211
20211
  }
@@ -20258,7 +20258,7 @@ function logoutCommand(opts = {}) {
20258
20258
  const released = releaseClaimForDir(target);
20259
20259
  if (!released) {
20260
20260
  console.error(`No agent claim for this directory (${target}).`);
20261
- console.error("Log in with `pitboss login`, or pass --all to sweep claims whose working directories are gone.");
20261
+ console.error("Log in with `shipster login`, or pass --all to sweep claims whose working directories are gone.");
20262
20262
  process.exit(2);
20263
20263
  }
20264
20264
  console.log(`Released claim for ${released.agentName} (${released.agentId}) on ${released.workingDir}.`);
@@ -20467,7 +20467,7 @@ async function requestDeviceGrant(api, proposed, rerun) {
20467
20467
  if (started.status !== 201) {
20468
20468
  const said = started.payload?.error?.message;
20469
20469
  throw new Error(
20470
- started.status === 429 ? "Too many join requests from your network just now. Wait a minute and try again." : said ?? `Could not reach pitboss at ${api} (HTTP ${started.status})`
20470
+ started.status === 429 ? "Too many join requests from your network just now. Wait a minute and try again." : said ?? `Could not reach shipster at ${api} (HTTP ${started.status})`
20471
20471
  );
20472
20472
  }
20473
20473
  const { data, deviceCode } = started.payload;
@@ -20534,7 +20534,7 @@ function wakeFiles(workerType, root) {
20534
20534
  path: join8(root, ".cursor", "rules", "pitboss.mdc"),
20535
20535
  write: (p) => {
20536
20536
  if (existsSync5(p) && !readFileSync8(p, "utf-8").includes("description: Pitboss wake-ups")) {
20537
- throw new Error(`${p} exists and was not written by pitboss; leaving it alone`);
20537
+ throw new Error(`${p} exists and was not written by Shipster or Pitboss; leaving it alone`);
20538
20538
  }
20539
20539
  writeRepoFile(p, cursorRule(cli, root), root);
20540
20540
  }
@@ -20712,7 +20712,7 @@ async function run(opts) {
20712
20712
  console.log(source_default.yellow("\n Restart this session before using pitboss."));
20713
20713
  console.log(source_default.dim(" A running MCP client keeps the tool schema it got at initialize."));
20714
20714
  console.log(source_default.dim("\n To add another agent, on this machine or any other:"));
20715
- console.log(source_default.dim(" give it its own worktree, run pitboss join there,"));
20715
+ console.log(source_default.dim(" give it its own worktree, run shipster join there,"));
20716
20716
  console.log(source_default.dim(` and pick ${repoName} on the approval screen.`));
20717
20717
  console.log("");
20718
20718
  }
@@ -20782,7 +20782,7 @@ function resolveAgentCreds({ dir, env: env3, cliConfig }) {
20782
20782
  if (cliConfig) {
20783
20783
  return validate2({ apiUrl: cliConfig.apiUrl, token: cliConfig.apiToken, agentId: cliConfig.agentId, repoId: cliConfig.repoId }, "cli-config");
20784
20784
  }
20785
- return { ok: false, reason: `no pitboss agent configured for ${start}: run \`pitboss join\` there` };
20785
+ return { ok: false, reason: `no shipster agent configured for ${start}: run \`shipster join\` there` };
20786
20786
  }
20787
20787
 
20788
20788
  // src/commands/watch.ts
@@ -20978,7 +20978,7 @@ async function watchOnce(dir, since) {
20978
20978
  }
20979
20979
  return printAndExit(wakeLine(wake.first, wake.count, agentId), 0);
20980
20980
  } catch (err) {
20981
- console.error(`pitboss watch: ${err.message}; reconnecting in ${Math.round(backoffMs / 1e3)}s`);
20981
+ console.error(`shipster watch: ${err.message}; reconnecting in ${Math.round(backoffMs / 1e3)}s`);
20982
20982
  await sleep2(backoffMs);
20983
20983
  backoffMs = Math.min(backoffMs * 2, 3e4);
20984
20984
  }
@@ -21040,7 +21040,7 @@ async function runStream(o) {
21040
21040
  throw new Error("event stream closed");
21041
21041
  } catch (err) {
21042
21042
  if (o.signal?.aborted) break;
21043
- o.warn(`pitboss watch: ${err.message}; reconnecting in ${Math.round(backoffMs / 1e3)}s`);
21043
+ o.warn(`shipster watch: ${err.message}; reconnecting in ${Math.round(backoffMs / 1e3)}s`);
21044
21044
  await sleep2(backoffMs);
21045
21045
  backoffMs = Math.min(backoffMs * 2, 3e4);
21046
21046
  }
@@ -21104,7 +21104,7 @@ async function watchStatus(o) {
21104
21104
  return { line: `cursor=${cursor} waiting=${waiting}${truncated ? "+" : ""}` };
21105
21105
  }
21106
21106
  function usageError(message) {
21107
- console.error(`pitboss watch: ${message}`);
21107
+ console.error(`shipster watch: ${message}`);
21108
21108
  process.exit(2);
21109
21109
  }
21110
21110
  async function statusCommand2(dir) {
@@ -21123,7 +21123,7 @@ async function statusCommand2(dir) {
21123
21123
  if ("refusal" in status2) return refuse2(status2.refusal);
21124
21124
  console.log(status2.line);
21125
21125
  } catch (err) {
21126
- console.error(`pitboss watch --status: ${err.message}`);
21126
+ console.error(`shipster watch --status: ${err.message}`);
21127
21127
  process.exit(1);
21128
21128
  }
21129
21129
  }
@@ -21145,7 +21145,7 @@ async function watchCommand(options) {
21145
21145
  const key = wakeKey(agentId, dir);
21146
21146
  if (since) applySince(state, key, since);
21147
21147
  const resuming = readCursor(state, key) !== null;
21148
- console.log(source_default.bold("pitboss watch") + source_default.dim(` \u2014 ${agentId}`));
21148
+ console.log(source_default.bold("shipster watch") + source_default.dim(` \u2014 ${agentId}`));
21149
21149
  console.log(source_default.dim(
21150
21150
  `Streaming live events you're subscribed to. Ctrl-C to stop.` + (kinds ? ` Filtering: ${[...kinds].join(", ")}` : "") + (resuming ? `
21151
21151
  (Events since the last one printed are replayed first.)` : `
@@ -21611,16 +21611,16 @@ function isMutableSource(path3) {
21611
21611
  return !NOT_SOURCE.some((part) => rooted.includes(part));
21612
21612
  }
21613
21613
  var pathspec = (paths) => paths && paths.length > 0 ? ["--", ...paths] : [];
21614
- function changedLines({ base: base2, cwd, paths, exec: exec4 = defaultExec }) {
21615
- const diff = exec4("git", ["diff", "-U0", "--no-color", `${base2}...HEAD`, ...pathspec(paths)], { cwd });
21614
+ function changedLines({ base: base2, cwd, paths, exec: exec5 = defaultExec }) {
21615
+ const diff = exec5("git", ["diff", "-U0", "--no-color", `${base2}...HEAD`, ...pathspec(paths)], { cwd });
21616
21616
  const lines3 = /* @__PURE__ */ new Map();
21617
21617
  for (const [path3, numbers] of parseUnifiedZero(diff)) {
21618
21618
  if (isMutableSource(path3)) lines3.set(path3, numbers);
21619
21619
  }
21620
21620
  return lines3;
21621
21621
  }
21622
- function changedTestFiles({ base: base2, cwd, paths, exec: exec4 = defaultExec }) {
21623
- const out = exec4("git", ["diff", "--name-only", "--diff-filter=AM", `${base2}...HEAD`, ...pathspec(paths)], { cwd });
21622
+ function changedTestFiles({ base: base2, cwd, paths, exec: exec5 = defaultExec }) {
21623
+ const out = exec5("git", ["diff", "--name-only", "--diff-filter=AM", `${base2}...HEAD`, ...pathspec(paths)], { cwd });
21624
21624
  return out.split("\n").filter((path3) => TEST.test(path3));
21625
21625
  }
21626
21626
 
@@ -21976,8 +21976,8 @@ function importPattern(file) {
21976
21976
  const base2 = basename2(file).replace(/\.[^.]+$/, "").replace(/[.+^${}()|[\]\\*?]/g, "\\$&");
21977
21977
  return `/${base2}(\\.[jt]sx?|\\.mjs)?['"]`;
21978
21978
  }
21979
- function directTestFiles(pkgDir, file, exec4 = defaultGrep) {
21980
- const out = exec4("git", ["grep", "-lE", importPattern(file), "--", "*.test.ts", "*.test.tsx", "*.test.js", "*.test.mjs"], pkgDir);
21979
+ function directTestFiles(pkgDir, file, exec5 = defaultGrep) {
21980
+ const out = exec5("git", ["grep", "-lE", importPattern(file), "--", "*.test.ts", "*.test.tsx", "*.test.js", "*.test.mjs"], pkgDir);
21981
21981
  return out.split("\n").filter(Boolean).map((rel) => resolve5(pkgDir, rel));
21982
21982
  }
21983
21983
  function defaultGrep(cmd, args, cwd) {
@@ -22199,11 +22199,13 @@ async function executeMutants(input) {
22199
22199
  };
22200
22200
  const message = (error) => error instanceof Error ? error.message : String(error);
22201
22201
  try {
22202
+ const totalDeadline = input.totalBudgetMs === void 0 ? Infinity : now() + input.totalBudgetMs;
22202
22203
  const files = [...new Set(input.candidates.map((c) => c.file))];
22203
22204
  const baselines = /* @__PURE__ */ new Map();
22204
22205
  const fileQueue = [...files];
22205
22206
  const baselineWorker = async (workerIndex) => {
22206
22207
  for (; ; ) {
22208
+ if (now() >= totalDeadline) return;
22207
22209
  const file = fileQueue.shift();
22208
22210
  if (file === void 0) return;
22209
22211
  try {
@@ -22221,7 +22223,11 @@ async function executeMutants(input) {
22221
22223
  const slowSeen = /* @__PURE__ */ new Set();
22222
22224
  for (const candidate of input.candidates) {
22223
22225
  const base2 = baselines.get(candidate.file);
22224
- if (base2 === null || base2 === void 0) {
22226
+ if (base2 === void 0) {
22227
+ out.skippedDeadline += 1;
22228
+ continue;
22229
+ }
22230
+ if (base2 === null) {
22225
22231
  out.errored += 1;
22226
22232
  continue;
22227
22233
  }
@@ -22249,7 +22255,7 @@ async function executeMutants(input) {
22249
22255
  okMs.push(baselines.get(candidate.file).ms);
22250
22256
  }
22251
22257
  const baselineMs = okMs.length > 0 ? okMs.reduce((a, b) => a + b, 0) / okMs.length : 1;
22252
- const deadline = now() + input.budgetMs;
22258
+ const deadline = Math.min(now() + input.budgetMs, totalDeadline);
22253
22259
  const planned = plannedCount({ budgetMs: input.budgetMs, workers: input.workers, baselineMs });
22254
22260
  const sampled = sampleMutants(runnable, planned, input.riskyPatterns);
22255
22261
  out.skippedSampled = sampled.skipped.length;
@@ -22339,6 +22345,7 @@ async function runMutateDiff(options) {
22339
22345
  session,
22340
22346
  workers,
22341
22347
  budgetMs: options.budgetMs,
22348
+ totalBudgetMs: options.totalBudgetMs,
22342
22349
  baselineSkipMs: options.baselineSkipMs ?? DEFAULT_BASELINE_SKIP_MS,
22343
22350
  riskyPatterns: config.riskyPatterns
22344
22351
  });
@@ -22377,6 +22384,7 @@ async function mutateDiffCommand(opts) {
22377
22384
  cwd: opts.cwd ?? process.cwd(),
22378
22385
  base: base2,
22379
22386
  budgetMs: positive(opts.budget, 120) * 1e3,
22387
+ totalBudgetMs: opts.totalBudget === void 0 ? void 0 : positive(opts.totalBudget, 0) * 1e3 || void 0,
22380
22388
  timeoutMs: positive(opts.timeout, 60) * 1e3,
22381
22389
  baselineTimeoutMs: positive(opts.baselineTimeout, 300) * 1e3,
22382
22390
  paths: opts.paths ? opts.paths.split(",").map((p) => p.trim()).filter(Boolean) : void 0
@@ -22676,7 +22684,8 @@ You implement specs on a branch and open a PR. Write production code and tests.
22676
22684
 
22677
22685
  **Playbook rules:**
22678
22686
  - Take the PR head sha from the PR itself (\`gh pr view <n> --json headRefOid\`), never from a handoff message or a task comment.
22679
- - One branch per order, cut from the default branch: never stack on another order's unmerged branch.`;
22687
+ - One branch per order, cut from the default branch: never stack on another order's unmerged branch.
22688
+ - A test may delete only paths under a directory it created with mkdtemp in that run. Never remove, empty or clear /tmp, $TMPDIR or any other shared directory as a whole, and never point a cleanup or reset at the real one.`;
22680
22689
  case "tester":
22681
22690
  return `## Your role: Tester
22682
22691
 
@@ -22690,7 +22699,8 @@ Write, fix, and run tests on the branch in branchName metadata.
22690
22699
  - If tests cannot pass due to an implementation bug: \`mcp__relai__add_task_comment\`
22691
22700
  (type: "finding") with type "bug", severity "blocking", then
22692
22701
  \`mcp__relai__assign_task\` back to the writer (previous assignee or list_agents).
22693
- Do not complete.`;
22702
+ Do not complete.
22703
+ - A test may delete only paths under a directory it created with mkdtemp in that run. Never remove, empty or clear /tmp, $TMPDIR or any other shared directory as a whole, and never point a cleanup or reset at the real one.`;
22694
22704
  case "devops":
22695
22705
  return `## Your role: DevOps
22696
22706
 
@@ -22706,6 +22716,7 @@ Handle CI/CD, infrastructure, build/deploy configuration.
22706
22716
  throw new Error(`Unknown specialization "${spec}" \u2014 valid values: reviewer, architect, writer, tester, devops`);
22707
22717
  }
22708
22718
  }
22719
+ var PR_FROM_ORDER = "In this section `<prUrl>` is the `prUrl` that `mcp__relai__get_order` returns for `metadata.orderId`. Never use the task's own `metadata.prUrl`. If the `epicId` get_order returns is not this task's own `epicId`, if get_order has no `prUrl`, or the task's `metadata.prUrl` is set and differs from it, run no `gh` command: block the task with a question naming the orchestrator, and stop.";
22709
22720
  var ORDER_RULES = `## Factory orders
22710
22721
 
22711
22722
  An order's tasks carry
@@ -22774,7 +22785,7 @@ Find the default branch with \`git remote show origin\`.
22774
22785
  is done. With one gate, block it on the build tasks alone.
22775
22786
  - \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
22776
22787
  5. One merge task, blocked by every gate task (or every build task when there are no gates),
22777
- \`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
22788
+ \`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge" }\`. The server stamps \`prUrl\` on it from the order, so you may omit it.
22778
22789
  If the order has \`pairBuilds\`, every build task needs \`verifyKind: "reviewer_agent"\` and a
22779
22790
  \`verifyReviewerId\` on a different \`workerType\` from the writers (see \`mcp__relai__list_agents\`);
22780
22791
  the API refuses a paired build task without one and routes it away from the reviewer's vendor.
@@ -22787,26 +22798,28 @@ Find the default branch with \`git remote show origin\`.
22787
22798
  \`metadata.orderKind: "review"\`): the goal is one review posted on the existing pull request at the
22788
22799
  order's \`prUrl\`. There is no order branch, no draft PR and no merge, so skip steps 2 to 6 above.
22789
22800
  Nobody pushes to the PR's branch, approves it, or requests changes on it.
22801
+ ${PR_FROM_ORDER}
22790
22802
  1. As the planner, read the PR with \`gh pr view <prUrl>\` and \`gh pr diff <prUrl>\`. Pick the
22791
22803
  angles it needs: correctness, security, simplicity, tests and consistency with the codebase for a
22792
22804
  real change; correctness and consistency alone for a small, low-risk one (docs, tests, under about
22793
22805
  150 changed lines with no auth, input handling, secrets, money or migrations).
22794
22806
  2. One task per angle, \`epicId\` = the order's \`epicId\`,
22795
- \`metadata: { orderId, orderRole: "review", angle: "<angle>", prUrl }\`. Spread the angles
22807
+ \`metadata: { orderId, orderRole: "review", angle: "<angle>" }\`; the server stamps \`prUrl\`. Spread the angles
22796
22808
  across vendors: assign each to a different agent from \`mcp__relai__list_agents\`, preferring a
22797
22809
  different \`workerType\` from the agents already given one, and reuse agents only when there are
22798
22810
  more angles than agents. Each description names the angle, the PR, what that angle hunts for, and
22799
22811
  the reporting format below.
22800
22812
  3. One lead task, \`blockedBy\` every angle task, assigned to yourself, with
22801
- \`metadata: { orderId, orderRole: "review_lead", prUrl }\`.
22813
+ \`metadata: { orderId, orderRole: "review_lead" }\`.
22802
22814
  4. When acceptance includes \`human_approval\`, add a gate task blocked by the lead
22803
22815
  (\`metadata: { orderId, orderRole: "gate" }\`, \`verifyKind: "human_approval"\`) and a post task
22804
- blocked by the gate (\`metadata: { orderId, orderRole: "post_review", prUrl }\`).
22816
+ blocked by the gate (\`metadata: { orderId, orderRole: "post_review" }\`).
22805
22817
  5. If the order has \`relatedRepos\`, copy them into every angle and lead task's metadata and name them in
22806
22818
  each description, so reviewers check the contracts this PR depends on in those repos.
22807
22819
  6. Post the task list on the planning task's thread and complete it.
22808
22820
 
22809
22821
  **orderRole "review"** (one angle of a review goal):
22822
+ - ${PR_FROM_ORDER} For the PR head, \`<n>\` is that PR's number.
22810
22823
  - Read the diff and as much surrounding code as you need. To run anything, check out the PR head
22811
22824
  detached: \`git fetch origin pull/<n>/head && git checkout --detach FETCH_HEAD\`. Never push, never
22812
22825
  comment on the PR, never edit the repo's history.
@@ -22822,6 +22835,7 @@ Nobody pushes to the PR's branch, approves it, or requests changes on it.
22822
22835
  - Post the report with \`mcp__relai__add_task_comment\` on this task, then complete it.
22823
22836
 
22824
22837
  **orderRole "review_lead"**:
22838
+ - ${PR_FROM_ORDER}
22825
22839
  - Read every angle task's report with \`mcp__relai__get_task_comments\`. Merge duplicates, drop
22826
22840
  findings you cannot reproduce or whose premise is wrong, and rank what is left. Keep an UNPROVEN mark
22827
22841
  on any finding about a related repo nobody could read. Severity sets the
@@ -22834,7 +22848,7 @@ Nobody pushes to the PR's branch, approves it, or requests changes on it.
22834
22848
  and put the review URL on the task's thread. With a gate, post the review as a comment on this task
22835
22849
  for the owner to approve, and do not touch the PR.
22836
22850
 
22837
- **orderRole "post_review"**: once the gate is approved, post the lead's review comment from the
22851
+ **orderRole "post_review"**: ${PR_FROM_ORDER} Once the gate is approved, post the lead's review comment from the
22838
22852
  lead task's thread with \`gh pr review <prUrl> --comment --body-file <file>\`, put the review URL on
22839
22853
  this task's thread, and complete.
22840
22854
 
@@ -22864,6 +22878,8 @@ this task's thread, and complete.
22864
22878
  the rest; if it is not recorded, reject and name it so the builder records it.
22865
22879
 
22866
22880
  **orderRole "merge"**:
22881
+ ${PR_FROM_ORDER}
22882
+
22867
22883
  **Playbook rules:**
22868
22884
  - Report a merge you cannot clear under step 3 the same minute: post which check failed, at which sha, and who must act on the merge task's thread, then set the task blocked.
22869
22885
  - Never merge on a stale head: re-read the PR head sha immediately before merging, pass it as \`--match-head-commit\`, and stop if it differs from the sha the gates approved, or the carried head after an Exit 0 conflict-only remerge.
@@ -22963,28 +22979,28 @@ var defaultExec2 = (file, args, opts) => new Promise((resolve19, reject) => {
22963
22979
  function validBranch(name) {
22964
22980
  return BRANCH_RE.test(name) && !name.startsWith("-") && !name.includes("..") && !name.endsWith("/") && !name.endsWith(".lock");
22965
22981
  }
22966
- function git2(exec4, repoPath, args, maxBuffer = LESSONS_MAX_BUFFER) {
22967
- return exec4("git", args, { cwd: repoPath, timeout: GIT_TIMEOUT_MS, maxBuffer });
22982
+ function git2(exec5, repoPath, args, maxBuffer = LESSONS_MAX_BUFFER) {
22983
+ return exec5("git", args, { cwd: repoPath, timeout: GIT_TIMEOUT_MS, maxBuffer });
22968
22984
  }
22969
- async function remoteBranchSha(exec4, repoPath, branch) {
22985
+ async function remoteBranchSha(exec5, repoPath, branch) {
22970
22986
  try {
22971
- const out = (await git2(exec4, repoPath, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}^{commit}`])).trim();
22987
+ const out = (await git2(exec5, repoPath, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}^{commit}`])).trim();
22972
22988
  return SHA_RE.test(out) ? out : null;
22973
22989
  } catch {
22974
22990
  return null;
22975
22991
  }
22976
22992
  }
22977
- async function resolveDefaultBranch(repoPath, exec4) {
22993
+ async function resolveDefaultBranch(repoPath, exec5) {
22978
22994
  const candidates = [];
22979
22995
  try {
22980
- const out = (await git2(exec4, repoPath, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).trim();
22996
+ const out = (await git2(exec5, repoPath, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).trim();
22981
22997
  if (out.startsWith("origin/")) candidates.push(out.slice("origin/".length));
22982
22998
  } catch {
22983
22999
  }
22984
23000
  candidates.push("main", "master");
22985
23001
  for (const branch of candidates) {
22986
23002
  if (!validBranch(branch)) continue;
22987
- const sha = await remoteBranchSha(exec4, repoPath, branch);
23003
+ const sha = await remoteBranchSha(exec5, repoPath, branch);
22988
23004
  if (sha) return { branch, sha };
22989
23005
  }
22990
23006
  return null;
@@ -23000,10 +23016,10 @@ function entryFor(repoPath, sha) {
23000
23016
  }
23001
23017
  return entry;
23002
23018
  }
23003
- async function blobAt(exec4, repoPath, ref, path3) {
23019
+ async function blobAt(exec5, repoPath, ref, path3) {
23004
23020
  let listing;
23005
23021
  try {
23006
- listing = await git2(exec4, repoPath, ["ls-tree", "-z", ref, "--", path3]);
23022
+ listing = await git2(exec5, repoPath, ["ls-tree", "-z", ref, "--", path3]);
23007
23023
  } catch {
23008
23024
  return null;
23009
23025
  }
@@ -23012,35 +23028,35 @@ async function blobAt(exec4, repoPath, ref, path3) {
23012
23028
  const match = row.match(/^(\d{6}) (\w+) ([0-9a-f]{40})\t(.+)$/);
23013
23029
  if (!match || match[4] !== path3 || match[2] !== "blob" || !BLOB_MODES.has(match[1])) return null;
23014
23030
  try {
23015
- return await git2(exec4, repoPath, ["cat-file", "blob", match[3]]);
23031
+ return await git2(exec5, repoPath, ["cat-file", "blob", match[3]]);
23016
23032
  } catch {
23017
23033
  return null;
23018
23034
  }
23019
23035
  }
23020
- async function readLessons(repoPath, exec4, resolved) {
23036
+ async function readLessons(repoPath, exec5, resolved) {
23021
23037
  for (const path3 of LESSONS_PATHS) {
23022
- const text = await blobAt(exec4, repoPath, resolved.sha, path3);
23038
+ const text = await blobAt(exec5, repoPath, resolved.sha, path3);
23023
23039
  if (text === void 0) continue;
23024
23040
  if (text === null || text.includes("\0")) return null;
23025
23041
  return { text, branch: resolved.branch, sha: resolved.sha };
23026
23042
  }
23027
23043
  return null;
23028
23044
  }
23029
- async function loadLessons(repoPath, exec4) {
23030
- const resolved = await resolveDefaultBranch(repoPath, exec4);
23045
+ async function loadLessons(repoPath, exec5) {
23046
+ const resolved = await resolveDefaultBranch(repoPath, exec5);
23031
23047
  if (!resolved) return null;
23032
23048
  const entry = entryFor(repoPath, resolved.sha);
23033
- if (!entry.lessons) entry.lessons = readLessons(repoPath, exec4, resolved);
23049
+ if (!entry.lessons) entry.lessons = readLessons(repoPath, exec5, resolved);
23034
23050
  return entry.lessons;
23035
23051
  }
23036
- async function loadTree(repoPath, exec4) {
23037
- const resolved = await resolveDefaultBranch(repoPath, exec4);
23052
+ async function loadTree(repoPath, exec5) {
23053
+ const resolved = await resolveDefaultBranch(repoPath, exec5);
23038
23054
  if (!resolved) return null;
23039
23055
  const entry = entryFor(repoPath, resolved.sha);
23040
23056
  if (!entry.tree) {
23041
23057
  entry.tree = (async () => {
23042
23058
  try {
23043
- const out = await git2(exec4, repoPath, ["ls-tree", "-r", "-z", "--name-only", resolved.sha], TREE_MAX_BUFFER);
23059
+ const out = await git2(exec5, repoPath, ["ls-tree", "-r", "-z", "--name-only", resolved.sha], TREE_MAX_BUFFER);
23044
23060
  const names2 = out.split("\0").filter((n) => n !== "");
23045
23061
  if (names2.length > MAX_TREE_ENTRIES) return null;
23046
23062
  return new Set(names2);
@@ -23492,8 +23508,8 @@ async function lessonsBlock(config, deps = {}) {
23492
23508
  try {
23493
23509
  const budget = budgetFromEnv(deps.env ?? process.env);
23494
23510
  if (budget === 0) return "";
23495
- const exec4 = deps.exec ?? defaultExec2;
23496
- const loaded = await loadLessons(config.repoPath, exec4);
23511
+ const exec5 = deps.exec ?? defaultExec2;
23512
+ const loaded = await loadLessons(config.repoPath, exec5);
23497
23513
  if (!loaded) return "";
23498
23514
  const { sections, warnings } = parseLessons(loaded.text);
23499
23515
  const droppedCount = warnings.filter((w) => w === "dropped_instruction").length;
@@ -23501,7 +23517,7 @@ async function lessonsBlock(config, deps = {}) {
23501
23517
  if (sections.length === 0) return warnings.length > 0 ? skip("unusable_file") : "";
23502
23518
  let paths = [];
23503
23519
  if (sections.some((s) => s.paths !== null)) {
23504
- const tree = await loadTree(config.repoPath, exec4);
23520
+ const tree = await loadTree(config.repoPath, exec5);
23505
23521
  if (tree) {
23506
23522
  const tasks = await (deps.fetchTasks ?? listSessionTaskRows)(config);
23507
23523
  paths = candidatePaths(tasks, tree);
@@ -23608,19 +23624,19 @@ async function fetchRepoRow(config, repoId, doFetch) {
23608
23624
  return { reachable: false };
23609
23625
  }
23610
23626
  }
23611
- async function originOf(root, exec4) {
23627
+ async function originOf(root, exec5) {
23612
23628
  try {
23613
- const out = (await exec4("git", ["remote", "get-url", "origin"], { cwd: root, timeout: GIT_TIMEOUT_MS, maxBuffer: 16 * 1024 })).trim();
23629
+ const out = (await exec5("git", ["remote", "get-url", "origin"], { cwd: root, timeout: GIT_TIMEOUT_MS, maxBuffer: 16 * 1024 })).trim();
23614
23630
  return out === "" ? null : out;
23615
23631
  } catch {
23616
23632
  return null;
23617
23633
  }
23618
23634
  }
23619
- async function checkoutMatches(repoPath, repoUrl, exec4) {
23635
+ async function checkoutMatches(repoPath, repoUrl, exec5) {
23620
23636
  if (repoUrl === null) return null;
23621
23637
  try {
23622
23638
  const root = await realpath(repoPath);
23623
- const origin = await originOf(root, exec4);
23639
+ const origin = await originOf(root, exec5);
23624
23640
  if (origin === null) return null;
23625
23641
  return normalizeRepoUrl(origin) === normalizeRepoUrl(repoUrl) ? root : null;
23626
23642
  } catch {
@@ -23629,14 +23645,14 @@ async function checkoutMatches(repoPath, repoUrl, exec4) {
23629
23645
  }
23630
23646
  async function repoContextBlock(target, config, deps = {}) {
23631
23647
  const log = deps.log ?? ((line) => console.log(line));
23632
- const exec4 = deps.exec ?? defaultExec2;
23648
+ const exec5 = deps.exec ?? defaultExec2;
23633
23649
  const doFetch = deps.fetch ?? ((...args) => fetch(...args));
23634
23650
  const row = await fetchRepoRow(config, target.repoId, doFetch);
23635
23651
  if (!row.reachable) {
23636
23652
  log("[context] skipped: repo_unreachable");
23637
23653
  return "";
23638
23654
  }
23639
- const root = await checkoutMatches(target.repoPath, row.repoUrl, exec4);
23655
+ const root = await checkoutMatches(target.repoPath, row.repoUrl, exec5);
23640
23656
  if (root === null) {
23641
23657
  log("[context] skipped: path_mismatch");
23642
23658
  return "";
@@ -23651,7 +23667,7 @@ async function repoContextBlock(target, config, deps = {}) {
23651
23667
  repoPath: root,
23652
23668
  specialization: target.role
23653
23669
  },
23654
- { exec: exec4, env: deps.env, log, fetchTasks: deps.fetchTasks }
23670
+ { exec: exec5, env: deps.env, log, fetchTasks: deps.fetchTasks }
23655
23671
  );
23656
23672
  if (lessons !== "") parts.push(lessons);
23657
23673
  const merged = await fetchRepoProfile(config, target.repoId, { fetch: doFetch });
@@ -24210,7 +24226,7 @@ function claudeArgs(opts) {
24210
24226
  ...opts.isolateUserConfig ? ["--setting-sources", "project,local"] : []
24211
24227
  ];
24212
24228
  }
24213
- async function runClaudeSession(config) {
24229
+ async function runClaudeSession(config, opts = {}) {
24214
24230
  const repoContextOn = readEnv("REPO_CONTEXT") === "1";
24215
24231
  const lessonsPromise = repoContextOn ? Promise.resolve("") : lessonsBlock({ ...config, repoId: taskRepoId(config) });
24216
24232
  const repoContextPromise = repoContextOn ? (readEnv("TASK_REPO_ID")?.trim() ? Promise.resolve(taskRepoId(config)) : contextRepoId(config)).then((repoId) => repoContextBlock({ repoId, repoPath: config.repoPath, role: config.specialization }, config)) : Promise.resolve("");
@@ -24235,7 +24251,8 @@ async function runClaudeSession(config) {
24235
24251
  const proc = spawn3(config.claudeBin, args, {
24236
24252
  cwd: config.repoPath,
24237
24253
  env: { ...childEnv, PATH: process.env.PATH },
24238
- stdio: ["pipe", "pipe", "pipe"]
24254
+ stdio: ["pipe", "pipe", "pipe"],
24255
+ signal: opts.signal
24239
24256
  });
24240
24257
  console.log(`[claude-worker] Process PID: ${proc.pid}`);
24241
24258
  Promise.all([lessonsPromise, repoContextPromise]).then(([lessons, repoContext2]) => {
@@ -24394,7 +24411,7 @@ function readCursorEvent(line) {
24394
24411
  }
24395
24412
  return {};
24396
24413
  }
24397
- async function runCursorSession(config) {
24414
+ async function runCursorSession(config, opts = {}) {
24398
24415
  const model = pinnedModel();
24399
24416
  recordLaunchedModel(model);
24400
24417
  const args = cursorArgs(config.repoPath, buildPrompt(config, { lessons: await lessonsBlock({ ...config, repoId: taskRepoId(config) }) }), model);
@@ -24402,7 +24419,7 @@ async function runCursorSession(config) {
24402
24419
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
24403
24420
  const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
24404
24421
  return new Promise((resolve19, reject) => {
24405
- const proc = spawn4(bin(), args, { cwd: config.repoPath, env: workerEnv("cursor", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
24422
+ const proc = spawn4(bin(), args, { cwd: config.repoPath, env: workerEnv("cursor", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
24406
24423
  console.log(`[cursor-worker] Process PID: ${proc.pid}`);
24407
24424
  const toolsUsed = [];
24408
24425
  let buffer = "";
@@ -24514,7 +24531,7 @@ function readCopilotEvent(line) {
24514
24531
  if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
24515
24532
  return {};
24516
24533
  }
24517
- async function runCopilotSession(config) {
24534
+ async function runCopilotSession(config, opts = {}) {
24518
24535
  const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
24519
24536
  const model = pinnedModel();
24520
24537
  recordLaunchedModel(model);
@@ -24523,7 +24540,7 @@ async function runCopilotSession(config) {
24523
24540
  const taskIdsPromise = listSessionTaskIds(config, "[copilot-worker]");
24524
24541
  console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
24525
24542
  return new Promise((resolve19, reject) => {
24526
- const proc = spawn5(bin2(), args, { cwd: config.repoPath, env: workerEnv("copilot", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
24543
+ const proc = spawn5(bin2(), args, { cwd: config.repoPath, env: workerEnv("copilot", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
24527
24544
  console.log(`[copilot-worker] Process PID: ${proc.pid}`);
24528
24545
  const toolsUsed = [];
24529
24546
  let buffer = "";
@@ -24641,7 +24658,7 @@ function readCodexEvent(line) {
24641
24658
  if (event.type === "error") return { error: String(event.message ?? "codex error") };
24642
24659
  return {};
24643
24660
  }
24644
- async function runCodexSession(config) {
24661
+ async function runCodexSession(config, opts = {}) {
24645
24662
  const profile = `pitboss-${config.agentId}`;
24646
24663
  mkdirSync9(codexHome(), { recursive: true, mode: 448 });
24647
24664
  const profilePath = join17(codexHome(), `${profile}.config.toml`);
@@ -24667,7 +24684,7 @@ async function runCodexSession(config) {
24667
24684
  const taskIdsPromise = listSessionTaskIds(config, "[codex-worker]");
24668
24685
  console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
24669
24686
  return new Promise((resolve19, reject) => {
24670
- const proc = spawn6(bin3(), args, { cwd: config.repoPath, env: workerEnv("codex", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
24687
+ const proc = spawn6(bin3(), args, { cwd: config.repoPath, env: workerEnv("codex", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
24671
24688
  console.log(`[codex-worker] Process PID: ${proc.pid}`);
24672
24689
  const toolsUsed = [];
24673
24690
  let buffer = "";
@@ -25117,6 +25134,10 @@ function seatKindOf(a, i, platform2) {
25117
25134
  }
25118
25135
  var ENV_REFUSED = (where) => `${where}: ownerPool entries take no env: account pools take no env grants, because every flagged repo's tasks would receive those variables`;
25119
25136
  var OWNER_POOL_RUNTIMES = ["claude", "codex"];
25137
+ var OWNER_POOL_SPECIALIZATIONS = ["writer", "architect", "reviewer", "tester", "devops"];
25138
+ function ownerPoolSpecializationCheck(label, specialization, what) {
25139
+ if (!OWNER_POOL_SPECIALIZATIONS.includes(specialization)) throw new Error(`${label}: ownerPool ${what} mint ${OWNER_POOL_SPECIALIZATIONS.join(", ")} only; specialization "${specialization}" is not one of them`);
25140
+ }
25120
25141
  function ownerPoolRuntimeCheck(label, runtime) {
25121
25142
  if (!OWNER_POOL_RUNTIMES.includes(runtime)) throw new Error(`${label}: ownerPool entries run claude or codex only; the per-session reset cannot keep a ${runtime} seat's login apart from its config`);
25122
25143
  }
@@ -25149,7 +25170,7 @@ function parsePool(a, i, specDir, platform2) {
25149
25170
  const runtime = runtimeOf(a, i);
25150
25171
  if (ownerPool) ownerPoolRuntimeCheck(`agents[${i}]`, runtime);
25151
25172
  const specialization = specializationOf(a, i);
25152
- if (ownerPool && specialization !== "writer") throw new Error(`agents[${i}]: ownerPool pools mint writers only; specialization must be writer`);
25173
+ if (ownerPool) ownerPoolSpecializationCheck(`agents[${i}]`, specialization, "pools");
25153
25174
  const max = a.max;
25154
25175
  if (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > MAX_POOL_SIZE) {
25155
25176
  throw new Error(`agents[${i}].max must be an integer from 1 to ${MAX_POOL_SIZE}`);
@@ -25205,6 +25226,7 @@ function parseSpec(text, specDir, opts = {}) {
25205
25226
  if (a.runAs === void 0) throw new Error(`${a.name}: ownerPool agents need runAs: without a seat user its home and config cannot be reset between sessions`);
25206
25227
  }
25207
25228
  const specialization = specializationOf(a, i);
25229
+ if (ownerPool) ownerPoolSpecializationCheck(a.name, specialization, "agents");
25208
25230
  agents.push({ name: a.name, runtime, worktree: resolve7(specDir, a.worktree), specialization, ...ownerPool ? { ownerPool: true } : tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...seatKindOf(a, i, platform2) });
25209
25231
  });
25210
25232
  const entries = [
@@ -25335,8 +25357,11 @@ function effectiveBounds(spec, desired) {
25335
25357
  const max = Math.min(desired.max, spec.max);
25336
25358
  return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
25337
25359
  }
25360
+ function busyCount(members) {
25361
+ return members.filter((m) => m.assigned + m.inProgress + (m.owedReviews ?? 0) > 0).length;
25362
+ }
25338
25363
  function reconcilePool({ pending, members, min, max, idleStopMs }) {
25339
- const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
25364
+ const isIdle = (m) => m.assigned === 0 && m.inProgress === 0 && (m.owedReviews ?? 0) === 0;
25340
25365
  const running = members.filter((m) => m.running);
25341
25366
  const stopped = members.filter((m) => !m.running);
25342
25367
  let count = running.length;
@@ -25360,23 +25385,35 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
25360
25385
  if (count >= min || count >= max) break;
25361
25386
  take(m);
25362
25387
  }
25363
- if (start.length > 0) return { start, stop: [] };
25388
+ if (start.length > 0) return { start, stop: [], why: {} };
25364
25389
  const stop = [];
25390
+ const why = {};
25365
25391
  for (const m of running.filter(isIdle)) {
25366
25392
  if (count <= max) break;
25367
25393
  stop.push(m.name);
25394
+ why[m.name] = "over-cap";
25368
25395
  count--;
25369
25396
  }
25370
- if (stop.length > 0) return { start: [], stop };
25397
+ if (stop.length > 0) return { start: [], stop, why };
25398
+ const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0).length;
25399
+ const freed = [];
25400
+ if (blockedOwners > 0 && count >= max) {
25401
+ for (const m of running.filter(isIdle)) {
25402
+ if (freed.length >= blockedOwners) break;
25403
+ freed.push(m.name);
25404
+ }
25405
+ if (freed.length > 0) return { start: [], stop: freed, why: Object.fromEntries(freed.map((n) => [n, "slot-for-owner"])) };
25406
+ }
25371
25407
  let surplus = running.filter(isIdle).length - pending;
25372
25408
  const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
25373
25409
  for (const m of eligible) {
25374
25410
  if (surplus <= 0 || count <= min) break;
25375
25411
  stop.push(m.name);
25412
+ why[m.name] = "idle-timer";
25376
25413
  count--;
25377
25414
  surplus--;
25378
25415
  }
25379
- return { start, stop };
25416
+ return { start, stop, why };
25380
25417
  }
25381
25418
  function poolTakes(served, t) {
25382
25419
  if (!served || served.length === 0) return true;
@@ -25385,12 +25422,12 @@ function poolTakes(served, t) {
25385
25422
  if (served.includes(declared)) return true;
25386
25423
  return served.some((d) => isDifficulty(d) && difficultyRung(d) === 2);
25387
25424
  }
25388
- function summarizeTasks(readyAll, heldAll, members, served) {
25425
+ function summarizeTasks(readyAll, heldAll, members, served, verifying = []) {
25389
25426
  const ready = readyAll.filter((t) => !t.paused);
25390
25427
  const held = heldAll.filter((t) => !(t.paused && t.status === "assigned"));
25391
25428
  const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
25392
25429
  const counts = Object.fromEntries(
25393
- members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
25430
+ members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0, owedReviews: 0, verifyingOwn: 0 }])
25394
25431
  );
25395
25432
  const pending = ready.filter((t) => t.status === "pending" && t.autoAssign && poolTakes(served, t)).length;
25396
25433
  let readyAssigned = 0;
@@ -25405,6 +25442,11 @@ function summarizeTasks(readyAll, heldAll, members, served) {
25405
25442
  if (!name) continue;
25406
25443
  if (t.status === "assigned") counts[name].assigned++;
25407
25444
  if (t.status === "in_progress" || t.status === "pending_verification") counts[name].inProgress++;
25445
+ if (t.status === "pending_verification") counts[name].verifyingOwn++;
25446
+ }
25447
+ for (const t of verifying) {
25448
+ const name = t.status === "pending_verification" && t.verifyReviewerId ? nameOf.get(t.verifyReviewerId) : void 0;
25449
+ if (name) counts[name].owedReviews++;
25408
25450
  }
25409
25451
  return { pending, ready: pending + readyAssigned, members: counts };
25410
25452
  }
@@ -25560,11 +25602,11 @@ async function abandonMinted(spec, agent, minted, reason) {
25560
25602
  return;
25561
25603
  }
25562
25604
  console.log(source_default.red(
25563
- ` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id} (HTTP ${res.status}); remove it with relai`
25605
+ ` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id} (HTTP ${res.status}); remove it with shipster`
25564
25606
  ));
25565
25607
  } catch {
25566
25608
  console.log(source_default.red(
25567
- ` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id}; remove it with relai`
25609
+ ` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id}; remove it with shipster`
25568
25610
  ));
25569
25611
  }
25570
25612
  }
@@ -25585,7 +25627,7 @@ function refuseNameClash(agent, existing, options) {
25585
25627
  ` \u2715 ${agent.name}: this repo already has an agent named ${agent.name} (id ${existing.id ?? "unknown"}, last seen ${existing.lastSeenAt ?? "never"}), and no credentials for it are in ${configDir()}`
25586
25628
  ));
25587
25629
  console.log(source_default.yellow(
25588
- ` run pitboss node up from the config directory that holds its credentials, or pass --adopt-new to register a new agent under the same name`
25630
+ ` run shipster node up from the config directory that holds its credentials, or pass --adopt-new to register a new agent under the same name`
25589
25631
  ));
25590
25632
  }
25591
25633
  async function redeemInvite(spec, agent, code, have, specPath, options) {
@@ -25917,7 +25959,7 @@ async function onboardCommand(url, opts) {
25917
25959
  // src/commands/node.ts
25918
25960
  import { userInfo } from "node:os";
25919
25961
  import { execFileSync as execFileSync15, spawn as spawnChild, spawnSync as spawnSyncChild } from "node:child_process";
25920
- import { closeSync, existsSync as existsSync27, mkdirSync as mkdirSync25, openSync, readFileSync as readFileSync27, realpathSync as realpathSync13, rmSync as rmSync15 } from "node:fs";
25962
+ import { appendFileSync as appendFileSync3, closeSync, existsSync as existsSync27, mkdirSync as mkdirSync25, openSync, readFileSync as readFileSync27, realpathSync as realpathSync13, rmSync as rmSync15 } from "node:fs";
25921
25963
  import { dirname as dirname24, resolve as resolve16 } from "node:path";
25922
25964
  import { fileURLToPath as fileURLToPath3 } from "node:url";
25923
25965
 
@@ -26911,7 +26953,7 @@ function resolveMcpServer(opts) {
26911
26953
  if (exists(bundled)) return { command: execPath, args: [bundled] };
26912
26954
  const dir = walkFor(opts.here, "mcp-server", exists);
26913
26955
  if (dir) return checkoutMcp(dir, execPath);
26914
- throw new Error("mcp-server not found next to this CLI. Run node from a pitboss checkout.");
26956
+ throw new Error("mcp-server not found next to this CLI. Run node from a shipster checkout.");
26915
26957
  }
26916
26958
  function isEventWorkerCommand(command) {
26917
26959
  return /\/tsx\/dist\/cli\.mjs \S*\/event-worker\/src\/index\.ts$/.test(command) || /(?:^|\s)\S*node(?:\.exe)? \S*\/event-worker\.js$/.test(command);
@@ -27734,9 +27776,9 @@ function prepareRoot(input, seats = false) {
27734
27776
  }
27735
27777
  async function enrollNode(input) {
27736
27778
  const code = input.code || readEnv("NODE_ENROLL_CODE");
27737
- if (!code) throw new EnrollError("No enrollment code. Pass it as an argument or set PITBOSS_NODE_ENROLL_CODE.", 2);
27779
+ if (!code) throw new EnrollError("No enrollment code. Pass it as an argument or set SHIPSTER_NODE_ENROLL_CODE (or PITBOSS_NODE_ENROLL_CODE).", 2);
27738
27780
  const api = (input.api || readConfig()?.apiUrl)?.replace(/\/+$/, "");
27739
- if (!api) throw new EnrollError("No API url. Pass --api <url>, or run `pitboss login` first.", 2);
27781
+ if (!api) throw new EnrollError("No API url. Pass --api <url>, or run `shipster login` first.", 2);
27740
27782
  if (!input.force && readNodeCreds()) {
27741
27783
  throw new EnrollError(`${nodeCredsPath()} already holds a node credential. To replace it, choose Re-enroll on the dashboard for a fresh code and pass --force.`);
27742
27784
  }
@@ -27888,6 +27930,13 @@ function toSpec(desired, ctx) {
27888
27930
  const name = assertName("agent name", a?.name, NAME2);
27889
27931
  const who = `agent ${show(name)}`;
27890
27932
  const runAs = seatOf(a, who, ctx.allow, users);
27933
+ if (repos.get(a.repo)?.kind === "account") {
27934
+ const { env: env3, tier } = a;
27935
+ const own = { ...common3({ ...a, env: void 0, tier: void 0 }, who, ctx.allow), env: env3, tier };
27936
+ const worktree2 = runAs ? seatDir(a, name) : dirIn(a, name, "agent").path;
27937
+ entries.push({ name, worktree: worktree2, ownerPool: true, ...own, ...runAs ? { runAs } : {} });
27938
+ continue;
27939
+ }
27891
27940
  const worktree = runAs ? seatDir(a, name) : dirIn(a, name, "agent").path;
27892
27941
  entries.push({ name, worktree, ...common3(a, who, ctx.allow), ...runAs ? { runAs } : {} });
27893
27942
  }
@@ -27895,6 +27944,19 @@ function toSpec(desired, ctx) {
27895
27944
  if (p?.ownerPool) throw new DesiredError("owner_pool_unsupported", `pool ${show(p?.pool)} is an owner-pool entry, which a node's desired state does not run`);
27896
27945
  const pool = assertName("pool name", p?.pool, POOL_NAME2);
27897
27946
  const seatUsers = poolSeatsOf(p, `pool ${show(pool)}`, ctx.allow, users);
27947
+ const home = repos.get(p.repo);
27948
+ if (home?.kind === "account") {
27949
+ const { env: env3, tier } = p;
27950
+ const own = { ...common3({ ...p, env: void 0, tier: void 0 }, `pool ${show(pool)}`, ctx.allow), env: env3, tier };
27951
+ let worktreeRoot = root;
27952
+ if (seatUsers) {
27953
+ for (let n = 1; n <= p.max; n++) seatDir(p, poolMemberName(pool, n));
27954
+ } else {
27955
+ worktreeRoot = dirIn(p, pool, "pool").path;
27956
+ }
27957
+ entries.push({ pool, ownerPool: true, worktreeRoot, ...own, ...seatUsers ? { runAs: { users: seatUsers } } : {}, difficulty: p.difficulty, min: p.min, max: p.max, burstMax: p.burstMax });
27958
+ continue;
27959
+ }
27898
27960
  if (seatUsers) {
27899
27961
  if (!repos.get(p.repo)) throw new DesiredError("repo_unavailable", `pool names repo ${show(p.repo)}, which this node cannot see`);
27900
27962
  for (let n = 1; n <= p.max; n++) seatDir(p, poolMemberName(pool, n));
@@ -28203,6 +28265,10 @@ async function retireAgent(name, d) {
28203
28265
  d.log(`${name}: its credential belongs to another server; it is left alone`);
28204
28266
  return false;
28205
28267
  }
28268
+ if (creds.desiredRoot !== d.root) {
28269
+ d.log(`${name}: its credential was not written by this node's root; it is left alone`);
28270
+ return false;
28271
+ }
28206
28272
  let status2;
28207
28273
  try {
28208
28274
  status2 = (await d.deleteJson(`${d.api}/agents/${creds.agentId}`, creds.token)).status;
@@ -28285,7 +28351,7 @@ function createGrantIdentity(deps) {
28285
28351
  const declinedUntil = /* @__PURE__ */ new Map();
28286
28352
  const have = (e) => {
28287
28353
  const c = deps.readCreds(e.agent.name);
28288
- return c && c.api === deps.api && c.worktree === e.agent.worktree && c.repoId === e.repo.id ? c : null;
28354
+ return c && c.desiredRoot === deps.root && c.api === deps.api && c.worktree === e.agent.worktree && c.repoId === e.repo.id ? c : null;
28289
28355
  };
28290
28356
  async function redeem(group, repoId, invites) {
28291
28357
  const { matched } = matchInvites(group.map((e) => e.agent), invites);
@@ -28497,6 +28563,7 @@ async function provisionEntries(entries, wanted, deps) {
28497
28563
  for (const p of wanted.pools) for (const m of poolMembers(p)) poolOf2.set(m.name, p);
28498
28564
  const pools = /* @__PURE__ */ new Map();
28499
28565
  for (const { agent, repo: repo2 } of entries) {
28566
+ if (agent.ownerPool) continue;
28500
28567
  const pool = poolOf2.get(agent.name);
28501
28568
  if (!pool) {
28502
28569
  await cloneInto(agent.worktree, repo2, deps);
@@ -28615,6 +28682,7 @@ async function provisionSeatClones(entries, deps) {
28615
28682
  };
28616
28683
  for (const { agent, repo: repo2 } of entries) {
28617
28684
  if (!agent.runAs) continue;
28685
+ if (agent.ownerPool) continue;
28618
28686
  const user = agent.runAs.user;
28619
28687
  const dest = agent.worktree;
28620
28688
  const uid = deps.seatUid(user);
@@ -28664,12 +28732,64 @@ async function provisionSeatClones(entries, deps) {
28664
28732
  return refused;
28665
28733
  }
28666
28734
 
28667
- // src/node/owner-pool-bases.ts
28735
+ // src/node/owner-pool-slot-guard.ts
28668
28736
  import { execFile as execFile8 } from "node:child_process";
28669
- import { chmodSync as chmodSync7, existsSync as existsSync22, lstatSync as lstatSync6, mkdirSync as mkdirSync21, readdirSync as readdirSync6, rmSync as rmSync12 } from "node:fs";
28670
- import { dirname as dirname19, join as join34 } from "node:path";
28671
28737
  import { promisify as promisify3 } from "node:util";
28672
28738
  var exec3 = promisify3(execFile8);
28739
+ async function inspectCheckout(user, dest) {
28740
+ const runAs = realRunAsDeps();
28741
+ const git3 = async (...args) => {
28742
+ const full = ["-C", dest, ...args];
28743
+ if (!user) return (await exec3("git", full, { cwd: "/", timeout: 6e4 })).stdout;
28744
+ const info = runAs.resolveUser(user);
28745
+ const env3 = { PATH: process.env.PATH, HOME: info?.home ?? "/", USER: user, LOGNAME: user };
28746
+ const launch = planRunAsLaunch(user, "git", full, runAs);
28747
+ return (await exec3(launch.command, launch.args, { cwd: "/", env: env3, timeout: 6e4 })).stdout;
28748
+ };
28749
+ try {
28750
+ await git3("rev-parse", "--git-dir");
28751
+ } catch {
28752
+ return null;
28753
+ }
28754
+ let origin = null;
28755
+ try {
28756
+ origin = (await git3("config", "--get", "remote.origin.url")).trim() || null;
28757
+ } catch {
28758
+ origin = null;
28759
+ }
28760
+ if (origin === null) return { origin, dirty: false, unpushed: false, stashes: false };
28761
+ const dirty = (await git3("status", "--porcelain", "--untracked-files=all")).trim() !== "";
28762
+ const unpushed = (await git3("rev-list", "--branches", "--not", "--remotes", "-n", "1")).trim() !== "";
28763
+ const stashes = (await git3("stash", "list")).trim() !== "";
28764
+ return { origin, dirty, unpushed, stashes };
28765
+ }
28766
+ var realSlotGuardDeps = () => ({ inspect: inspectCheckout });
28767
+ async function refuseDirtySlots(agents, deps) {
28768
+ const refused = /* @__PURE__ */ new Map();
28769
+ for (const a of agents) {
28770
+ if (!a.ownerPool) continue;
28771
+ let facts;
28772
+ try {
28773
+ facts = await deps.inspect(a.runAs?.user, a.worktree);
28774
+ } catch (err) {
28775
+ const why = (err instanceof Error ? err.message : String(err)).split("\n")[0].slice(0, 160);
28776
+ refused.set(a.name, `owner_pool_slot_dirty: ${a.worktree} could not be inspected, so it is not reset: ${why}`);
28777
+ continue;
28778
+ }
28779
+ if (!facts || facts.origin === null) continue;
28780
+ const held = [facts.dirty && "uncommitted files", facts.unpushed && "unpushed commits", facts.stashes && "stashes"].filter(Boolean);
28781
+ if (held.length === 0) continue;
28782
+ refused.set(a.name, `owner_pool_slot_dirty: ${a.worktree} is a checkout holding ${held.join(", ")}, and an owner-pool slot is wiped before every session. Push or discard that work, or empty the directory, then the next generation adopts it.`);
28783
+ }
28784
+ return refused;
28785
+ }
28786
+
28787
+ // src/node/owner-pool-bases.ts
28788
+ import { execFile as execFile9 } from "node:child_process";
28789
+ import { chmodSync as chmodSync7, existsSync as existsSync22, lstatSync as lstatSync6, mkdirSync as mkdirSync21, readdirSync as readdirSync6, rmSync as rmSync12 } from "node:fs";
28790
+ import { dirname as dirname19, join as join34 } from "node:path";
28791
+ import { promisify as promisify4 } from "node:util";
28792
+ var exec4 = promisify4(execFile9);
28673
28793
  var GROUP_OTHER_WRITE2 = 18;
28674
28794
  var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
28675
28795
  var OWN_TASK_STATUSES = "status=assigned,in_progress";
@@ -28809,13 +28929,13 @@ function realOwnerPoolBasesDeps(root, env3 = process.env, tokenHosts = [], log =
28809
28929
  const runAs = realRunAsDeps();
28810
28930
  const git3 = async (args, gitEnv) => {
28811
28931
  try {
28812
- await exec3("git", args, { env: credentialFree ? credentialFreeEnv(gitEnv) : gitEnv, timeout: 10 * 6e4 });
28932
+ await exec4("git", args, { env: credentialFree ? credentialFreeEnv(gitEnv) : gitEnv, timeout: 10 * 6e4 });
28813
28933
  } catch (err) {
28814
28934
  throw new Error(err.stderr || (err instanceof Error ? err.message : String(err)));
28815
28935
  }
28816
28936
  };
28817
28937
  const lockdown = async (dest) => {
28818
- await exec3("chmod", ["-R", "go-w", dest]);
28938
+ await exec4("chmod", ["-R", "go-w", dest]);
28819
28939
  chmodSync7(dest, 493);
28820
28940
  chmodSync7(dirname19(dest), 493);
28821
28941
  chmodSync7(root, 493);
@@ -29318,7 +29438,7 @@ function createCriticalNotifier(deps) {
29318
29438
  `Disk critical on a node: ${describeDisk(d)}.`,
29319
29439
  `Critical means under ${t.critGb} GB or ${t.critPct}% free.`,
29320
29440
  `A janitor dry run would free ${freed}.`,
29321
- `On the node, run: pitboss node janitor --dry-run`
29441
+ `On the node, run: shipster node janitor --dry-run`
29322
29442
  ].join("\n");
29323
29443
  };
29324
29444
  async function send(d, ep) {
@@ -29727,7 +29847,7 @@ async function runJanitor(opts) {
29727
29847
  const empty = { removed: [], skipped: [], bytes: 0, capReached: false, errors: [] };
29728
29848
  const base2 = { mode, dry: true, report: empty, skippedByReason: {}, removedBytes: 0, wouldRemoveBytes: 0, capReached: false, dailyCapReached: false, usedTodayBytes: 0, heldByFloor: 0, rebuildableOff: [], items: [], at };
29729
29849
  if (mode === "live" && !settings.enabled) {
29730
- return { ...base2, status: "refused", exitCode: 2, message: "this node has opted out of the janitor; run `pitboss node janitor --enable` to turn deletion back on, or use --dry-run" };
29850
+ return { ...base2, status: "refused", exitCode: 2, message: "this node has opted out of the janitor; run `shipster node janitor --enable` to turn deletion back on, or use --dry-run" };
29731
29851
  }
29732
29852
  const dry = mode === "dry" || !settings.enabled;
29733
29853
  const runId = randomBytes11(6).toString("hex");
@@ -30178,9 +30298,9 @@ async function runDocker(client, handle, now, cfg) {
30178
30298
  }
30179
30299
 
30180
30300
  // src/node/janitor/docker-client.ts
30181
- import { execFile as execFile9 } from "node:child_process";
30301
+ import { execFile as execFile10 } from "node:child_process";
30182
30302
  var runDocker2 = (args) => new Promise((resolve19, reject) => {
30183
- execFile9("docker", args, { maxBuffer: 64 * 1024 * 1024, timeout: 6e4 }, (err, stdout, stderr) => {
30303
+ execFile10("docker", args, { maxBuffer: 64 * 1024 * 1024, timeout: 6e4 }, (err, stdout, stderr) => {
30184
30304
  if (!err) {
30185
30305
  resolve19(stdout);
30186
30306
  return;
@@ -30444,9 +30564,9 @@ function isBusy(path3, agents, now = Date.now(), walkLimit) {
30444
30564
  }
30445
30565
 
30446
30566
  // src/node/janitor/pr-state.ts
30447
- import { execFile as execFile10 } from "node:child_process";
30448
- import { promisify as promisify4 } from "node:util";
30449
- var execFileP = promisify4(execFile10);
30567
+ import { execFile as execFile11 } from "node:child_process";
30568
+ import { promisify as promisify5 } from "node:util";
30569
+ var execFileP = promisify5(execFile11);
30450
30570
  var realGh = async (cwd, args) => {
30451
30571
  try {
30452
30572
  const { stdout } = await execFileP("gh", args, {
@@ -30494,11 +30614,11 @@ function makePrStateOf(deps) {
30494
30614
  }
30495
30615
 
30496
30616
  // src/node/janitor/worktrees.ts
30497
- import { execFile as execFile11 } from "node:child_process";
30617
+ import { execFile as execFile12 } from "node:child_process";
30498
30618
  import { existsSync as existsSync24, readdirSync as readdirSync9, realpathSync as realpathSync11, statSync as statSync6 } from "node:fs";
30499
30619
  import { dirname as dirname22, isAbsolute as isAbsolute4, join as join41, relative as relative4, sep as sep6 } from "node:path";
30500
- import { promisify as promisify5 } from "node:util";
30501
- var execFileP2 = promisify5(execFile11);
30620
+ import { promisify as promisify6 } from "node:util";
30621
+ var execFileP2 = promisify6(execFile12);
30502
30622
  var RULE = "worktrees";
30503
30623
  function decideWorktree(f) {
30504
30624
  if (f.primary) return { action: "skip", reason: "primary" };
@@ -30976,7 +31096,7 @@ function logStoredOrphans(specNames, specRealpath) {
30976
31096
  if (orphans.length > 0) {
30977
31097
  console.log(source_default.yellow(`
30978
31098
  stored credentials no longer in the spec: ${orphans.join(", ")}`));
30979
- console.log(source_default.dim(" pitboss node prune <spec> to revoke them"));
31099
+ console.log(source_default.dim(" shipster node prune <spec> to revoke them"));
30980
31100
  }
30981
31101
  if (unattributed.length > 0) {
30982
31102
  console.log(source_default.yellow(` unattributed, not pruned: ${unattributed.join(", ")}`));
@@ -31487,7 +31607,18 @@ function startPoolLoop(run4, sup, record, account) {
31487
31607
  let lastSize;
31488
31608
  const markedAt = /* @__PURE__ */ new Map();
31489
31609
  const state = { members: members.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null, ...pool.ownerPool ? { ownerPool: true } : {} };
31490
- const log = (line) => console.log(`${(/* @__PURE__ */ new Date()).toISOString()} pool ${pool.pool}: ${line}`);
31610
+ const poolLog = logPath(`${pool.pool}.pool`);
31611
+ const log = (line) => {
31612
+ const text = `${(/* @__PURE__ */ new Date()).toISOString()} pool ${pool.pool}: ${line}`;
31613
+ console.log(text);
31614
+ try {
31615
+ mkdirSync25(dirname24(poolLog), { recursive: true, mode: 448 });
31616
+ appendFileSync3(poolLog, `${text}
31617
+ `, { mode: 384 });
31618
+ } catch {
31619
+ return;
31620
+ }
31621
+ };
31491
31622
  let busy = false;
31492
31623
  const markStopped = async (member) => {
31493
31624
  try {
@@ -31497,10 +31628,10 @@ function startPoolLoop(run4, sup, record, account) {
31497
31628
  log(`could not mark ${member.agent.name} unavailable: ${err instanceof Error ? err.message : String(err)}`);
31498
31629
  }
31499
31630
  };
31500
- const report = async (member, action, reason, ready) => {
31631
+ const report = async (member, action, reason, ready, size2) => {
31501
31632
  const running = members.filter((m) => sup.isActive(m.agent.name)).length;
31502
31633
  try {
31503
- await reportScale(member.creds, { pool: pool.pool, action, reason, ready, running, max: pool.max });
31634
+ await reportScale(member.creds, { pool: pool.pool, action, reason, ready, running, max: pool.max, size: size2 });
31504
31635
  } catch (err) {
31505
31636
  log(`could not report ${action} ${member.agent.name}: ${err instanceof Error ? err.message : String(err)}`);
31506
31637
  }
@@ -31518,15 +31649,16 @@ function startPoolLoop(run4, sup, record, account) {
31518
31649
  fetchTasks(reader.creds, "ready=true", pool.ownerPool === true),
31519
31650
  fetchTasks(reader.creds, "status=assigned,in_progress,pending_verification", pool.ownerPool === true)
31520
31651
  ]);
31652
+ const verifying = held.filter((t) => t.status === "pending_verification");
31521
31653
  const { size: desired, capacityMode } = await fetchPoolSize(reader.creds, pool.pool);
31522
31654
  if (pool.ownerPool && run4.bases) {
31523
31655
  await run4.bases.sync(pool.pool, members.map((m) => ({ name: m.agent.name, runAs: m.agent.runAs, creds: m.creds })));
31524
31656
  }
31525
- const summary2 = summarizeTasks(ready, held, members.map((m) => ({ name: m.agent.name, agentId: m.creds.agentId })), desired?.served);
31657
+ const summary2 = summarizeTasks(ready, held, members.map((m) => ({ name: m.agent.name, agentId: m.creds.agentId })), desired?.served, verifying);
31526
31658
  const now = Date.now();
31527
31659
  const views = members.map(({ agent }) => {
31528
31660
  const counts = summary2.members[agent.name];
31529
- if (counts.assigned + counts.inProgress > 0) lastBusy.set(agent.name, now);
31661
+ if (counts.assigned + counts.inProgress + counts.owedReviews > 0) lastBusy.set(agent.name, now);
31530
31662
  return { name: agent.name, running: sup.isActive(agent.name), ...counts, idleMs: now - (lastBusy.get(agent.name) ?? now) };
31531
31663
  });
31532
31664
  const bounds = effectiveBounds(pool, desired);
@@ -31549,7 +31681,7 @@ function startPoolLoop(run4, sup, record, account) {
31549
31681
  max: bounds.max,
31550
31682
  burstMax: pool.burstMax,
31551
31683
  provisioned: pool.max,
31552
- busy: views.filter((v) => v.assigned + v.inProgress > 0).length,
31684
+ busy: busyCount(views),
31553
31685
  idleWithNoReadyWork: summary2.ready === 0 && views.every((v) => v.assigned + v.inProgress === 0),
31554
31686
  currentAccount,
31555
31687
  snapshot: readCapacity(capacityDir(), now),
@@ -31572,6 +31704,10 @@ function startPoolLoop(run4, sup, record, account) {
31572
31704
  lastSize = { max: bounds.max, size: sizing.size };
31573
31705
  Object.assign(state, { effectiveSize: sizing.size, burstMax: sizing.ceiling, capacityLevel: sizing.level, capacityUnused: sizing.unused });
31574
31706
  const decision = reconcilePool({ pending: summary2.pending, members: views, min: Math.min(bounds.min, sizing.size), max: sizing.size, idleStopMs });
31707
+ if (decision.start.length > 0 || decision.stop.length > 0) {
31708
+ const view = (v) => `${v.name}{${v.running ? "up" : "down"} assigned ${v.assigned} inProgress ${v.inProgress} readyAssigned ${v.readyAssigned} owed ${v.owedReviews} idle ${Math.round(v.idleMs / 1e3)}s}`;
31709
+ log(`decision start [${decision.start.join(", ")}] stop [${decision.stop.join(", ")}] pending ${summary2.pending} ready ${summary2.ready} min ${Math.min(bounds.min, sizing.size)} max ${sizing.size}: ${views.map(view).join(" ")}`);
31710
+ }
31575
31711
  for (const name of decision.start) {
31576
31712
  const member = members.find((m) => m.agent.name === name);
31577
31713
  try {
@@ -31588,14 +31724,17 @@ function startPoolLoop(run4, sup, record, account) {
31588
31724
  lastBusy.set(name, Date.now());
31589
31725
  sup.startAgent(name);
31590
31726
  log(`started ${name} (ready ${summary2.ready})`);
31591
- await report(member, "started", `ready ${summary2.ready}`, summary2.ready);
31727
+ await report(member, "started", `ready ${summary2.ready}`, summary2.ready, sizing.size);
31592
31728
  }
31729
+ const runningBefore = views.filter((v) => v.running).length;
31593
31730
  for (const name of decision.stop) {
31594
31731
  const member = members.find((m) => m.agent.name === name);
31595
31732
  await sup.stopAgent(name);
31596
31733
  await markStopped(member);
31597
- log(`stopped ${name} after ${Math.round(idleStopMs / 1e3)}s idle`);
31598
- await report(member, "stopped", `idle ${Math.round(idleStopMs / 1e3)}s`, summary2.ready);
31734
+ const branch = decision.why[name];
31735
+ const reason = branch === "over-cap" ? `over cap: size ${sizing.size} below ${runningBefore} running (${sizing.reason})`.slice(0, 200) : branch === "slot-for-owner" ? "slot freed for a stopped owner with work" : `idle ${Math.round(idleStopMs / 1e3)}s`;
31736
+ log(`stopped ${name}: ${reason}`);
31737
+ await report(member, "stopped", reason, summary2.ready, sizing.size);
31599
31738
  }
31600
31739
  Object.assign(state, { ready: summary2.ready, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: void 0 });
31601
31740
  } catch (err) {
@@ -31612,10 +31751,32 @@ function startPoolLoop(run4, sup, record, account) {
31612
31751
  return () => clearInterval(timer);
31613
31752
  }
31614
31753
  var MODEL_REFRESH_MS = 15e3;
31754
+ function desiredIdentity(api, root, nodeToken, log) {
31755
+ return createGrantIdentity({
31756
+ api,
31757
+ root,
31758
+ nodeToken,
31759
+ now: Date.now,
31760
+ log,
31761
+ readCreds: (n) => readCreds(n),
31762
+ writeCreds,
31763
+ workerType: workerTypeOf,
31764
+ post: (url, body, token2) => postJson(url, body, token2)
31765
+ });
31766
+ }
31767
+ function desiredReconcilerDeps(node2, log, io) {
31768
+ const root = node2.root;
31769
+ if (!root) throw new Error("the node record has no root");
31770
+ return {
31771
+ retire: (name) => retireAgent(name, { api: node2.api, root, readCreds: io.readCreds, removeCreds: io.removeCreds, deleteJson: io.deleteJson, log }),
31772
+ storedNames: () => desiredModeCredNames(io.listStoredAgentNames(), root, node2.api, io.readCreds),
31773
+ identity: desiredIdentity(node2.api, root, node2.token, log)
31774
+ };
31775
+ }
31615
31776
  async function nodeUpDesired() {
31616
31777
  try {
31617
31778
  const node2 = readNodeCreds();
31618
- if (!node2) throw new Error("no spec file given and this machine is not enrolled. Run `pitboss node enroll`, or pass a spec file.");
31779
+ if (!node2) throw new Error("no spec file given and this machine is not enrolled. Run `shipster node enroll`, or pass a spec file.");
31619
31780
  if (!node2.root) throw new Error(`${nodeCredsPath()} has no root. Re-enroll with a fresh code and --root <dir> (the directory worker clones live under).`);
31620
31781
  const root = node2.root;
31621
31782
  let previous = null;
@@ -31730,8 +31891,10 @@ async function nodeUpDesired() {
31730
31891
  const provisionDeps = realProvisionDeps(root, process.env, node2.allow?.hosts);
31731
31892
  const ownerBases = createOwnerPoolBases(realOwnerPoolBasesDeps(ownerPoolBasesRoot(configDir()), process.env, node2.allow?.hosts));
31732
31893
  const seatCloneDeps = realSeatCloneDeps(root, process.env, node2.allow?.hosts);
31894
+ const slotGuardDeps = realSlotGuardDeps();
31733
31895
  let seatRefusals = /* @__PURE__ */ new Map();
31734
31896
  const seats = trackSeats();
31897
+ const deps = desiredReconcilerDeps(node2, log, { readCreds, removeCreds, deleteJson, listStoredAgentNames });
31735
31898
  const reconciler = new Reconciler({
31736
31899
  root,
31737
31900
  api: node2.api,
@@ -31750,33 +31913,25 @@ async function nodeUpDesired() {
31750
31913
  latest = wanted;
31751
31914
  runner.expect(wanted);
31752
31915
  seats.provision(wanted);
31753
- await provisionEntries(entries.filter((e) => !e.agent.runAs), wanted, provisionDeps);
31754
- seatRefusals = await provisionSeatClones(entries.filter((e) => e.agent.runAs), seatCloneDeps);
31916
+ const slotRefusals = await refuseDirtySlots(entries.map((e) => e.agent), slotGuardDeps);
31917
+ const open = entries.filter((e) => !slotRefusals.has(e.agent.name));
31918
+ await provisionEntries(open.filter((e) => !e.agent.runAs), wanted, provisionDeps);
31919
+ seatRefusals = new Map([...await provisionSeatClones(open.filter((e) => e.agent.runAs), seatCloneDeps), ...slotRefusals]);
31755
31920
  },
31756
31921
  retire(name) {
31757
31922
  seats.retired(name);
31758
- return retireAgent(name, { api: node2.api, readCreds, removeCreds, deleteJson, log });
31923
+ return deps.retire(name);
31759
31924
  },
31760
31925
  pendingRetires,
31761
31926
  savePendingRetires(names2) {
31762
31927
  pendingRetires.splice(0, pendingRetires.length, ...names2);
31763
31928
  writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
31764
31929
  },
31765
- storedNames: () => desiredModeCredNames(listStoredAgentNames(), root, node2.api, readCreds),
31766
- identity: createGrantIdentity({
31767
- api: node2.api,
31768
- root,
31769
- nodeToken: node2.token,
31770
- now: Date.now,
31771
- log,
31772
- readCreds: (n) => readCreds(n),
31773
- writeCreds,
31774
- workerType: workerTypeOf,
31775
- post: (url, body, token2) => postJson(url, body, token2)
31776
- }),
31930
+ storedNames: deps.storedNames,
31931
+ identity: deps.identity,
31777
31932
  runner
31778
31933
  });
31779
- console.log(source_default.bold("\n pitboss node: desired state") + source_default.dim(` root ${root} state ${statePath2()}
31934
+ console.log(source_default.bold("\n shipster node: desired state") + source_default.dim(` root ${root} state ${statePath2()}
31780
31935
  `));
31781
31936
  const keepAlive = setInterval(() => {
31782
31937
  }, 6e4);
@@ -31915,7 +32070,7 @@ async function nodeUpCommand(specFile, opts = {}) {
31915
32070
  const plainCount = runnable.filter((a) => !pooled.has(a.name)).length;
31916
32071
  const poolNote = pools.length > 0 ? `, ${pools.length} pool(s)` : "";
31917
32072
  console.log(source_default.bold(`
31918
- pitboss node: ${plainCount} agent(s)${poolNote}`) + source_default.dim(` state ${statePath2()}
32073
+ shipster node: ${plainCount} agent(s)${poolNote}`) + source_default.dim(` state ${statePath2()}
31919
32074
  `));
31920
32075
  const stopLoops = [];
31921
32076
  const ownerBases = createOwnerPoolBases(ownerPoolBasesDepsFor(opts));
@@ -31992,7 +32147,7 @@ async function startRelayLogins(agents, needsLogin, onReady) {
31992
32147
  }
31993
32148
  if (!creds) {
31994
32149
  for (const a of seats) needsLogin.add(a.name);
31995
- console.log(source_default.yellow(` ! ${seats.length} relay seat(s) need a login but this machine has no node credential; run \`pitboss node enroll\``));
32150
+ console.log(source_default.yellow(` ! ${seats.length} relay seat(s) need a login but this machine has no node credential; run \`shipster node enroll\``));
31996
32151
  return null;
31997
32152
  }
31998
32153
  const manager = new LoginManager({
@@ -32066,7 +32221,7 @@ async function nodeEnrollCommand(code, opts) {
32066
32221
  allowRunAs: commaList(opts.allowRunAs)
32067
32222
  });
32068
32223
  console.log(`Enrolled as "${name}" (${nodeId}). Credential saved to ${path3}.`);
32069
- console.log("Start workers with: pitboss node up <spec.json>, or pitboss node up to follow the dashboard's desired state.");
32224
+ console.log("Start workers with: shipster node up <spec.json>, or shipster node up to follow the dashboard's desired state.");
32070
32225
  } catch (err) {
32071
32226
  console.error(source_default.red(`
32072
32227
  ${err instanceof Error ? err.message : String(err)}`));
@@ -32108,7 +32263,7 @@ function nodeStatusCommand(opts) {
32108
32263
  if (enrolled) console.log(nodeLine());
32109
32264
  const diskColor = { ok: source_default.green, warn: source_default.yellow, critical: source_default.red };
32110
32265
  for (const d of disk) console.log(` ${diskColor[d.level](describeDisk(d))}`);
32111
- if (disk.some((d) => d.level === "critical")) console.log(source_default.red(" next: pitboss node janitor --dry-run"));
32266
+ if (disk.some((d) => d.level === "critical")) console.log(source_default.red(" next: shipster node janitor --dry-run"));
32112
32267
  const { invalid } = diskThresholds();
32113
32268
  if (invalid.length > 0) console.log(source_default.yellow(` ! ignored ${invalid.join(", ")} (not a positive number within range); using the default`));
32114
32269
  console.log(`${supervised ? source_default.green("supervisor up") : source_default.red("supervisor down")} pid ${state.supervisorPid} ${source_default.dim(state.specPath)}`);
@@ -32134,15 +32289,15 @@ import { dirname as dirname25, join as join45, resolve as resolve17 } from "node
32134
32289
  import { createHash as createHash3 } from "node:crypto";
32135
32290
  import { realpathSync as realpathSync14 } from "node:fs";
32136
32291
  import { isAbsolute as isAbsolute5, join as join44 } from "node:path";
32137
- var UNSUPPORTED = "pitboss node service supports Linux (systemd --user) and macOS (launchd) only";
32138
- function serviceIdentity(configDir2, platform2) {
32292
+ var UNSUPPORTED = "shipster node service supports Linux (systemd --user) and macOS (launchd) only";
32293
+ function build(configDir2, platform2, names2) {
32139
32294
  if (platform2 !== "linux" && platform2 !== "darwin") throw new Error(UNSUPPORTED);
32140
32295
  if (!isAbsolute5(configDir2)) throw new Error("configDir must be an absolute path");
32141
32296
  const real3 = realpathSync14(configDir2);
32142
32297
  const hash = createHash3("sha256").update(real3).digest("hex").slice(0, 10);
32143
32298
  const logPath2 = join44(real3, "node", "logs", "service.log");
32144
32299
  if (platform2 === "linux") {
32145
- const label2 = `pitboss-node-${hash}`;
32300
+ const label2 = `${names2.systemd}${hash}`;
32146
32301
  const unitFileName2 = `${label2}.service`;
32147
32302
  return {
32148
32303
  label: label2,
@@ -32151,7 +32306,7 @@ function serviceIdentity(configDir2, platform2) {
32151
32306
  logPath: logPath2
32152
32307
  };
32153
32308
  }
32154
- const label = `dev.pitboss.node.${hash}`;
32309
+ const label = `${names2.launchd}${hash}`;
32155
32310
  const unitFileName = `${label}.plist`;
32156
32311
  return {
32157
32312
  label,
@@ -32160,6 +32315,12 @@ function serviceIdentity(configDir2, platform2) {
32160
32315
  logPath: logPath2
32161
32316
  };
32162
32317
  }
32318
+ function serviceIdentity(configDir2, platform2) {
32319
+ return build(configDir2, platform2, { systemd: "shipster-node-", launchd: "dev.shipster.node." });
32320
+ }
32321
+ function legacyServiceIdentity(configDir2, platform2) {
32322
+ return build(configDir2, platform2, { systemd: "pitboss-node-", launchd: "dev.pitboss.node." });
32323
+ }
32163
32324
 
32164
32325
  // src/node/service/input.ts
32165
32326
  import { isAbsolute as isAbsolute6 } from "node:path";
@@ -32168,20 +32329,20 @@ var SECRET_KEY = /TOKEN|SECRET|KEY|PASSWORD/i;
32168
32329
  var LINE_BREAK_OR_NUL = /[\0\r\n]/;
32169
32330
  var SYSTEMD_EXPANSION = /[%$`]/;
32170
32331
  var NPX_CACHE = ["/_npx/", "/.npm/_npx"];
32171
- var NPX_MESSAGE = "run the installed pitboss, not npx: npm install -g @pitboss/cli, then run pitboss node service install";
32332
+ var NPX_MESSAGE = "run the installed shipster, not npx: npm install -g @shipster/cli, then run shipster node service install";
32172
32333
  function checkServiceInput(input, syntax) {
32173
32334
  for (const key of Object.keys(input.env)) {
32174
32335
  if (SECRET_KEY.test(key)) {
32175
32336
  throw new Error(
32176
- `pitboss node service: env ${JSON.stringify(key)} looks like a secret; it is never written into a service file`
32337
+ `shipster node service: env ${JSON.stringify(key)} looks like a secret; it is never written into a service file`
32177
32338
  );
32178
32339
  }
32179
32340
  if (!ALLOWED_ENV_KEYS.includes(key)) {
32180
- throw new Error(`pitboss node service: env ${JSON.stringify(key)} is not an allowed env key`);
32341
+ throw new Error(`shipster node service: env ${JSON.stringify(key)} is not an allowed env key`);
32181
32342
  }
32182
32343
  }
32183
- if (!("PATH" in input.env)) throw new Error("pitboss node service: env PATH is required");
32184
- if (!("HOME" in input.env)) throw new Error("pitboss node service: env HOME is required");
32344
+ if (!("PATH" in input.env)) throw new Error("shipster node service: env PATH is required");
32345
+ if (!("HOME" in input.env)) throw new Error("shipster node service: env HOME is required");
32185
32346
  const texts = [
32186
32347
  ["nodeBin", input.nodeBin],
32187
32348
  ["cliEntry", input.cliEntry],
@@ -32192,11 +32353,11 @@ function checkServiceInput(input, syntax) {
32192
32353
  ];
32193
32354
  for (const [name, value] of texts) {
32194
32355
  if (LINE_BREAK_OR_NUL.test(value)) {
32195
- throw new Error(`pitboss node service: ${name} contains a line break or NUL byte; refusing`);
32356
+ throw new Error(`shipster node service: ${name} contains a line break or NUL byte; refusing`);
32196
32357
  }
32197
32358
  }
32198
32359
  for (const [name, value] of texts.slice(0, 4)) {
32199
- if (!isAbsolute6(value)) throw new Error(`pitboss node service: ${name} must be an absolute path`);
32360
+ if (!isAbsolute6(value)) throw new Error(`shipster node service: ${name} must be an absolute path`);
32200
32361
  }
32201
32362
  for (const path3 of [input.nodeBin, input.cliEntry]) {
32202
32363
  if (NPX_CACHE.some((marker) => path3.includes(marker))) throw new Error(NPX_MESSAGE);
@@ -32205,7 +32366,7 @@ function checkServiceInput(input, syntax) {
32205
32366
  for (const [name, value] of texts) {
32206
32367
  if (SYSTEMD_EXPANSION.test(value)) {
32207
32368
  throw new Error(
32208
- `pitboss node service: ${name} contains % $ or a backtick, which systemd would expand; refusing`
32369
+ `shipster node service: ${name} contains % $ or a backtick, which systemd would expand; refusing`
32209
32370
  );
32210
32371
  }
32211
32372
  }
@@ -32218,7 +32379,7 @@ function xmlEscape(value) {
32218
32379
  }
32219
32380
  function renderLaunchdPlist(input) {
32220
32381
  checkServiceInput(input, "launchd");
32221
- if (!/^[A-Za-z0-9.-]+$/.test(input.label)) throw new Error("pitboss node service: label is not a plain reverse-DNS name");
32382
+ if (!/^[A-Za-z0-9.-]+$/.test(input.label)) throw new Error("shipster node service: label is not a plain reverse-DNS name");
32222
32383
  const str = (value, indent) => `${indent}<string>${xmlEscape(value)}</string>`;
32223
32384
  const environment = [["PITBOSS_CONFIG_DIR", input.configDir], ...Object.entries(input.env)];
32224
32385
  const lines3 = [
@@ -32379,7 +32540,7 @@ function defaultServiceDeps() {
32379
32540
  };
32380
32541
  }
32381
32542
  function realConfigDir(deps) {
32382
- if (!existsSync28(deps.configDir)) throw new ServiceError(`config dir ${deps.configDir} does not exist; run pitboss node enroll first`);
32543
+ if (!existsSync28(deps.configDir)) throw new ServiceError(`config dir ${deps.configDir} does not exist; run shipster node enroll first`);
32383
32544
  return realpathSync15(deps.configDir);
32384
32545
  }
32385
32546
  function identityOf(deps, real3) {
@@ -32389,6 +32550,9 @@ function identityOf(deps, real3) {
32389
32550
  throw new ServiceError(err instanceof Error ? err.message : String(err));
32390
32551
  }
32391
32552
  }
32553
+ function legacyIdentityOf(deps, real3) {
32554
+ return legacyServiceIdentity(real3, deps.platform);
32555
+ }
32392
32556
  function nodeCredentialExists(deps) {
32393
32557
  return existsSync28(join45(deps.configDir, "node", "node.json"));
32394
32558
  }
@@ -32443,14 +32607,40 @@ function waitUntil(deps, probe2) {
32443
32607
  }
32444
32608
  return false;
32445
32609
  }
32610
+ function isRunning(deps, identity) {
32611
+ if (deps.platform !== "linux") return launchdState(deps, identity.label)?.state === "running";
32612
+ const st = systemdState(deps, identity.unitFileName);
32613
+ return st.activeState === "active" && st.subState === "running";
32614
+ }
32615
+ function removeUnit(deps, identity) {
32616
+ const unitPath = identity.unitPath(deps.home);
32617
+ if (!existsSync28(unitPath)) return null;
32618
+ if (deps.platform === "linux") {
32619
+ const off = deps.exec("systemctl", ["--user", "disable", "--now", identity.unitFileName]);
32620
+ if (off.status !== 0) deps.err(`WARNING: systemctl --user disable --now ${identity.unitFileName}: ${(off.stderr || off.stdout).trim()}`);
32621
+ unlinkSync5(unitPath);
32622
+ const reload = deps.exec("systemctl", ["--user", "daemon-reload"]);
32623
+ if (reload.status !== 0) deps.err("WARNING: systemctl --user daemon-reload failed");
32624
+ } else {
32625
+ if (launchdState(deps, identity.label) !== null) {
32626
+ const off = deps.exec("launchctl", ["bootout", `gui/${deps.uid}/${identity.label}`]);
32627
+ if (off.status !== 0) deps.err(`WARNING: launchctl bootout failed: ${(off.stderr || off.stdout).trim()}`);
32628
+ }
32629
+ unlinkSync5(unitPath);
32630
+ }
32631
+ return unitPath;
32632
+ }
32446
32633
  function reportNotRunning(deps, logPath2) {
32447
- deps.err(`pitboss node service: the node is not running after ${WAIT_ATTEMPTS} seconds. Last ${LOG_TAIL_LINES} lines of ${logPath2}:`);
32634
+ deps.err(`shipster node service: the node is not running after ${WAIT_ATTEMPTS} seconds. Last ${LOG_TAIL_LINES} lines of ${logPath2}:`);
32635
+ tailLog(deps, logPath2);
32636
+ return 1;
32637
+ }
32638
+ function tailLog(deps, logPath2) {
32448
32639
  if (existsSync28(logPath2)) {
32449
32640
  for (const line of readFileSync28(logPath2, "utf-8").replace(/\n$/, "").split("\n").slice(-LOG_TAIL_LINES)) deps.err(line);
32450
32641
  } else {
32451
32642
  deps.err("(no log yet)");
32452
32643
  }
32453
- return 1;
32454
32644
  }
32455
32645
  function startLinux(deps, identity, rewritten) {
32456
32646
  const systemctl = (args) => deps.exec("systemctl", ["--user", ...args]);
@@ -32477,14 +32667,60 @@ function startDarwin(deps, identity, plist, rewritten) {
32477
32667
  must(deps.exec("launchctl", ["enable", target]), "launchctl enable");
32478
32668
  deps.out("a LaunchAgent starts when you log in, not at boot: the node does not come back after a reboot until you log in");
32479
32669
  }
32670
+ var STABLE_POLLS = 3;
32671
+ function stopUnit(deps, identity) {
32672
+ if (deps.platform === "linux") {
32673
+ const off = deps.exec("systemctl", ["--user", "disable", "--now", identity.unitFileName]);
32674
+ if (off.status !== 0) deps.err(`WARNING: systemctl --user disable --now ${identity.unitFileName}: ${(off.stderr || off.stdout).trim()}`);
32675
+ } else if (launchdState(deps, identity.label) !== null) {
32676
+ const off = deps.exec("launchctl", ["bootout", `gui/${deps.uid}/${identity.label}`]);
32677
+ if (off.status !== 0) deps.err(`WARNING: launchctl bootout failed: ${(off.stderr || off.stdout).trim()}`);
32678
+ }
32679
+ }
32680
+ function restartUnit(deps, identity) {
32681
+ if (deps.platform === "linux") {
32682
+ const on2 = deps.exec("systemctl", ["--user", "enable", "--now", identity.unitFileName]);
32683
+ if (on2.status !== 0) deps.err(`WARNING: could not restart ${identity.unitFileName}: ${(on2.stderr || on2.stdout).trim()}`);
32684
+ return;
32685
+ }
32686
+ const target = `gui/${deps.uid}/${identity.label}`;
32687
+ const boot = deps.exec("launchctl", ["bootstrap", `gui/${deps.uid}`, identity.unitPath(deps.home)]);
32688
+ const on = deps.exec("launchctl", ["enable", target]);
32689
+ if (boot.status !== 0 || on.status !== 0) deps.err(`WARNING: could not restart ${identity.label}`);
32690
+ }
32691
+ function restartCount(deps, identity) {
32692
+ return deps.platform === "linux" ? systemdState(deps, identity.unitFileName).nRestarts : null;
32693
+ }
32694
+ function heartbeatSince(deps, since) {
32695
+ if (!nodeCredentialExists(deps)) return true;
32696
+ const hb = heartbeatOf(deps);
32697
+ return hb !== "unknown" && Date.parse(hb.at) >= since;
32698
+ }
32699
+ function waitStable(deps, identity, startedAt) {
32700
+ let baseline;
32701
+ let streak = 0;
32702
+ for (let attempt = 0; attempt < WAIT_ATTEMPTS; attempt++) {
32703
+ const count = restartCount(deps, identity);
32704
+ if (isRunning(deps, identity) && (baseline === void 0 || count === baseline)) {
32705
+ if (baseline === void 0) baseline = count;
32706
+ streak++;
32707
+ if (streak >= STABLE_POLLS && heartbeatSince(deps, startedAt)) return true;
32708
+ } else {
32709
+ baseline = void 0;
32710
+ streak = 0;
32711
+ }
32712
+ if (attempt < WAIT_ATTEMPTS - 1) deps.sleep(1e3);
32713
+ }
32714
+ return false;
32715
+ }
32480
32716
  function installCore(opts, deps) {
32481
32717
  const real3 = realConfigDir(deps);
32482
32718
  const identity = identityOf(deps, real3);
32483
32719
  if (!deps.cliEntry.startsWith("/") || !deps.cliEntry.endsWith(".js")) {
32484
- throw new ServiceError("pitboss node service needs the built CLI (dist/cli.js); install @pitboss/cli and run it from there");
32720
+ throw new ServiceError("shipster node service needs the built CLI (dist/cli.js); install @shipster/cli and run it from there");
32485
32721
  }
32486
32722
  if (opts.spec === void 0 && !nodeCredentialExists(deps)) {
32487
- throw new ServiceError(`no node credential under ${deps.configDir}/node: run pitboss node enroll first, or pass a spec file`);
32723
+ throw new ServiceError(`no node credential under ${deps.configDir}/node: run shipster node enroll first, or pass a spec file`);
32488
32724
  }
32489
32725
  const args = opts.spec === void 0 ? [] : [specArgument(opts.spec)];
32490
32726
  const input = {
@@ -32498,18 +32734,40 @@ function installCore(opts, deps) {
32498
32734
  const linux = deps.platform === "linux";
32499
32735
  const text = linux ? renderSystemdUnit(input) : renderLaunchdPlist({ ...input, label: identity.label });
32500
32736
  const unitPath = identity.unitPath(deps.home);
32737
+ const legacy = legacyIdentityOf(deps, real3);
32738
+ const legacyInstalled = existsSync28(legacy.unitPath(deps.home));
32501
32739
  mkdirSync26(dirname25(identity.logPath), { recursive: true, mode: 448 });
32502
- const rewritten = placeUnit(unitPath, text, opts.force === true);
32503
- deps.out(`${rewritten ? "wrote" : "unchanged"} ${unitPath}`);
32504
- if (linux) startLinux(deps, identity, rewritten);
32505
- else startDarwin(deps, identity, unitPath, rewritten);
32506
- const running = () => {
32507
- if (!linux) return launchdState(deps, identity.label)?.state === "running";
32508
- const st = systemdState(deps, identity.unitFileName);
32509
- return st.activeState === "active" && st.subState === "running";
32510
- };
32511
- if (!waitUntil(deps, running)) return reportNotRunning(deps, identity.logPath);
32740
+ const newExisted = existsSync28(unitPath);
32741
+ if (legacyInstalled) stopUnit(deps, legacy);
32742
+ const startedAt = Date.now();
32743
+ let healthy = false;
32744
+ let rewritten = false;
32745
+ try {
32746
+ rewritten = placeUnit(unitPath, text, opts.force === true);
32747
+ deps.out(`${rewritten ? "wrote" : "unchanged"} ${unitPath}`);
32748
+ if (linux) startLinux(deps, identity, rewritten);
32749
+ else startDarwin(deps, identity, unitPath, rewritten);
32750
+ healthy = legacyInstalled ? waitStable(deps, identity, startedAt) : waitUntil(deps, () => isRunning(deps, identity));
32751
+ } catch (err) {
32752
+ if (!legacyInstalled) throw err;
32753
+ deps.err(err instanceof Error ? err.message : String(err));
32754
+ }
32755
+ if (!healthy) {
32756
+ if (!legacyInstalled) return reportNotRunning(deps, identity.logPath);
32757
+ if (newExisted) stopUnit(deps, identity);
32758
+ else removeUnit(deps, identity);
32759
+ restartUnit(deps, legacy);
32760
+ deps.err(
32761
+ `shipster node service: the new unit ${identity.unitFileName} did not become healthy within ${WAIT_ATTEMPTS} seconds; ${newExisted ? "stopped" : "removed"} it and restarted ${legacy.unitFileName}. Last lines of ${identity.logPath}:`
32762
+ );
32763
+ tailLog(deps, identity.logPath);
32764
+ return 1;
32765
+ }
32512
32766
  deps.out(`running: ${identity.unitFileName}`);
32767
+ if (legacyInstalled) {
32768
+ const removed = removeUnit(deps, legacy);
32769
+ deps.out(`migrated from ${legacy.unitFileName}: removed ${removed}`);
32770
+ }
32513
32771
  return 0;
32514
32772
  }
32515
32773
  function serviceInstall(opts, deps) {
@@ -32534,7 +32792,8 @@ function heartbeatOf(deps) {
32534
32792
  }
32535
32793
  function statusOf(deps) {
32536
32794
  const real3 = realConfigDir(deps);
32537
- const identity = identityOf(deps, real3);
32795
+ const current = identityOf(deps, real3);
32796
+ const identity = !existsSync28(current.unitPath(deps.home)) && existsSync28(legacyIdentityOf(deps, real3).unitPath(deps.home)) ? legacyIdentityOf(deps, real3) : current;
32538
32797
  const unitPath = identity.unitPath(deps.home);
32539
32798
  const base2 = { unit: identity.unitFileName, unitPath, logPath: identity.logPath, heartbeat: heartbeatOf(deps) };
32540
32799
  if (!existsSync28(unitPath)) {
@@ -32599,26 +32858,12 @@ function serviceStatus(opts, deps) {
32599
32858
  function serviceUninstall(deps) {
32600
32859
  try {
32601
32860
  const real3 = realConfigDir(deps);
32602
- const identity = identityOf(deps, real3);
32603
- const unitPath = identity.unitPath(deps.home);
32604
- if (!existsSync28(unitPath)) {
32605
- deps.out(`not installed: ${unitPath} does not exist`);
32861
+ const removed = [identityOf(deps, real3), legacyIdentityOf(deps, real3)].map((identity) => removeUnit(deps, identity)).filter((path3) => path3 !== null);
32862
+ if (removed.length === 0) {
32863
+ deps.out(`not installed: ${identityOf(deps, real3).unitPath(deps.home)} does not exist`);
32606
32864
  return 0;
32607
32865
  }
32608
- if (deps.platform === "linux") {
32609
- const off = deps.exec("systemctl", ["--user", "disable", "--now", identity.unitFileName]);
32610
- if (off.status !== 0) deps.err(`WARNING: systemctl --user disable --now ${identity.unitFileName}: ${(off.stderr || off.stdout).trim()}`);
32611
- unlinkSync5(unitPath);
32612
- const reload = deps.exec("systemctl", ["--user", "daemon-reload"]);
32613
- if (reload.status !== 0) deps.err("WARNING: systemctl --user daemon-reload failed");
32614
- } else {
32615
- if (launchdState(deps, identity.label) !== null) {
32616
- const off = deps.exec("launchctl", ["bootout", `gui/${deps.uid}/${identity.label}`]);
32617
- if (off.status !== 0) deps.err(`WARNING: launchctl bootout failed: ${(off.stderr || off.stdout).trim()}`);
32618
- }
32619
- unlinkSync5(unitPath);
32620
- }
32621
- deps.out(`removed ${unitPath}`);
32866
+ for (const path3 of removed) deps.out(`removed ${path3}`);
32622
32867
  deps.out("node.json, logs and the config dir are left alone");
32623
32868
  return 0;
32624
32869
  } catch (err) {
@@ -32660,7 +32905,7 @@ function resolveJanitorTarget(d) {
32660
32905
  return { ok: true, mode: "spec", roots: plan2.roots, repos: plan2.repos, managed: plan2.managed, nodeId: d.identity()?.nodeId ?? "", optOut: plan2.optOut };
32661
32906
  }
32662
32907
  const node2 = d.identity();
32663
- if (!node2) return { ok: false, message: "this machine has no enrolled node with a work root; run `pitboss node enroll` first" };
32908
+ if (!node2) return { ok: false, message: "this machine has no enrolled node with a work root; run `shipster node enroll` first" };
32664
32909
  const listed = state?.managedWorktrees;
32665
32910
  const managed = Array.isArray(listed) ? listed.filter((p) => typeof p === "string" && isAbsolute7(p)) : [];
32666
32911
  return { ok: true, mode: "desired", roots: [node2.root], repos: [], managed, nodeId: node2.nodeId };
@@ -32712,7 +32957,7 @@ function print(r, capGb, dailyCapGb, healthyDays, roots) {
32712
32957
  return;
32713
32958
  }
32714
32959
  console.log(source_default.bold(`
32715
- pitboss node janitor: ${r.dry ? "dry run, nothing deleted" : "live"}`) + source_default.dim(` ${roots.join(", ")}`));
32960
+ shipster node janitor: ${r.dry ? "dry run, nothing deleted" : "live"}`) + source_default.dim(` ${roots.join(", ")}`));
32716
32961
  for (const i of r.items) console.log(` ${i.action === "removed" ? source_default.red("removed ") : source_default.yellow("would remove")} ${i.rule.padEnd(15)} ${size(i.bytes).padStart(9)} ${i.path}`);
32717
32962
  for (const s of r.report.skipped.filter((x) => !x.rule.startsWith("docker"))) console.log(source_default.dim(` skipped ${s.rule.padEnd(15)} ${s.reason} ${s.path}${s.branch ? ` (${s.branch})` : ""}`));
32718
32963
  for (const x of r.rebuildableOff) console.log(source_default.dim(` skipped rebuildable ${x.reason} ${x.path}`));
@@ -32856,14 +33101,14 @@ async function runDoctor(opts, deps) {
32856
33101
  const top = base2.exec("git", ["rev-parse", "--show-toplevel"], { cwd: base2.cwd });
32857
33102
  const root = top.stdout.trim();
32858
33103
  if (!top.ok || !root) {
32859
- deps.stderr(`pitboss doctor: ${base2.cwd} is not a git repo`);
33104
+ deps.stderr(`shipster doctor: ${base2.cwd} is not a git repo`);
32860
33105
  return 2;
32861
33106
  }
32862
33107
  const report = await runChecks(deps.checks, { ...base2, cwd: root });
32863
33108
  deps.stdout(opts.json ? renderJson(report) : renderTable(report, { color: deps.color }));
32864
33109
  return opts.strict && report.summary.fail > 0 ? 1 : 0;
32865
33110
  } catch (err) {
32866
- deps.stderr(`pitboss doctor: ${err instanceof Error ? err.message : String(err)}`);
33111
+ deps.stderr(`shipster doctor: ${err instanceof Error ? err.message : String(err)}`);
32867
33112
  return 2;
32868
33113
  }
32869
33114
  }
@@ -33200,7 +33445,7 @@ function playbookCheck(id, name, judge) {
33200
33445
  id,
33201
33446
  name,
33202
33447
  async run(env3) {
33203
- if (env3.api === null) return warn(id, name, "not logged in to pitboss", "run pitboss join");
33448
+ if (env3.api === null) return warn(id, name, "not logged in to shipster", "run shipster join");
33204
33449
  const facts = await fetchFacts(env3.api, env3);
33205
33450
  if (facts === "denied") return warn(id, name, "cannot read playbook facts for this repo", "check --repo and that this login can read the repo");
33206
33451
  return judge(facts);
@@ -33243,7 +33488,7 @@ var autoMergeCheck = featureCheck("auto-merge", "Auto-merge is enabled", (f) =>
33243
33488
  var conflictBotCheck = featureCheck("conflict-bot", "The conflict bot is enabled", (f) => f.conflictBot);
33244
33489
 
33245
33490
  // src/doctor/checks/install-health.ts
33246
- var JOIN = "run pitboss join";
33491
+ var JOIN = "run shipster join";
33247
33492
  function result(id, name, status2, reason, fix) {
33248
33493
  return { id, name, status: status2, reason, fix };
33249
33494
  }
@@ -33261,7 +33506,7 @@ var TOKEN_ID = "token-valid";
33261
33506
  var TOKEN_NAME = "The API accepts this login's token";
33262
33507
  var tokenCheck = installCheck(TOKEN_ID, TOKEN_NAME, async (install) => {
33263
33508
  if (!install.creds().ok) {
33264
- return result(TOKEN_ID, TOKEN_NAME, "fail", "no usable pitboss login for this folder", JOIN);
33509
+ return result(TOKEN_ID, TOKEN_NAME, "fail", "no usable shipster login for this folder", JOIN);
33265
33510
  }
33266
33511
  const code = await install.ping("agent");
33267
33512
  if (code === 401 || code === 403) return result(TOKEN_ID, TOKEN_NAME, "fail", "token revoked or invalid", JOIN);
@@ -33276,11 +33521,11 @@ var tokenCheck = installCheck(TOKEN_ID, TOKEN_NAME, async (install) => {
33276
33521
  );
33277
33522
  });
33278
33523
  var CONFIG_ID = "config-readable";
33279
- var CONFIG_NAME = "The pitboss config is readable and its API is reachable";
33524
+ var CONFIG_NAME = "The shipster config is readable and its API is reachable";
33280
33525
  var CONFIG_PROBLEM = {
33281
33526
  missing: "does not exist",
33282
33527
  unreadable: "cannot be read",
33283
- invalid: "is not a valid pitboss config"
33528
+ invalid: "is not a valid shipster config"
33284
33529
  };
33285
33530
  var configCheck = installCheck(CONFIG_ID, CONFIG_NAME, async (install) => {
33286
33531
  const found = install.creds();
@@ -33290,12 +33535,12 @@ var configCheck = installCheck(CONFIG_ID, CONFIG_NAME, async (install) => {
33290
33535
  const { state, file } = install.config();
33291
33536
  if (state !== "ok") return result(CONFIG_ID, CONFIG_NAME, "fail", `config file ${file} ${CONFIG_PROBLEM[state]}`, JOIN);
33292
33537
  const code = await install.ping("livez");
33293
- if (code === null) return result(CONFIG_ID, CONFIG_NAME, "fail", "the configured API is unreachable", "check the API url or your network, or run pitboss join");
33294
- if (code !== 200) return result(CONFIG_ID, CONFIG_NAME, "fail", `the configured API answered ${code} on /livez`, "check the API url or run pitboss join");
33538
+ if (code === null) return result(CONFIG_ID, CONFIG_NAME, "fail", "the configured API is unreachable", "check the API url or your network, or run shipster join");
33539
+ if (code !== 200) return result(CONFIG_ID, CONFIG_NAME, "fail", `the configured API answered ${code} on /livez`, "check the API url or run shipster join");
33295
33540
  return result(CONFIG_ID, CONFIG_NAME, "ok", `config file ${file} is readable and the API answers`, "");
33296
33541
  });
33297
33542
  var MCP_ID = "mcp-server";
33298
- var MCP_NAME = "The pitboss MCP server starts and answers a read-only call";
33543
+ var MCP_NAME = "The shipster MCP server starts and answers a read-only call";
33299
33544
  var mcpCheck = installCheck(MCP_ID, MCP_NAME, async (install) => {
33300
33545
  const r = await install.mcp();
33301
33546
  if (r.ok) return result(MCP_ID, MCP_NAME, "ok", "list_agents answered", "");
@@ -33304,13 +33549,13 @@ var mcpCheck = installCheck(MCP_ID, MCP_NAME, async (install) => {
33304
33549
  const apiDown = code === null || code >= 500 && code < 600 || code === 429;
33305
33550
  if (apiDown) return result(MCP_ID, MCP_NAME, "warn", `${r.reason}; the API did not answer cleanly`, "check the API and re-run");
33306
33551
  }
33307
- return result(MCP_ID, MCP_NAME, "fail", r.reason, "upgrade @pitboss/cli or run pitboss join");
33552
+ return result(MCP_ID, MCP_NAME, "fail", r.reason, "upgrade @shipster/cli or run shipster join");
33308
33553
  });
33309
33554
  var WATCH_ID = "watcher-config";
33310
33555
  var WATCH_NAME = "The watch config for this folder is valid";
33311
33556
  var watcherCheck = installCheck(WATCH_ID, WATCH_NAME, (install) => {
33312
33557
  const found = install.creds();
33313
- if (found.ok) return result(WATCH_ID, WATCH_NAME, "ok", `pitboss watch resolves its config from ${found.source}`, "");
33558
+ if (found.ok) return result(WATCH_ID, WATCH_NAME, "ok", `shipster watch resolves its config from ${found.source}`, "");
33314
33559
  return result(WATCH_ID, WATCH_NAME, "fail", `${REFUSED_MARKER} ${found.reason}`, JOIN);
33315
33560
  });
33316
33561
 
@@ -33338,7 +33583,7 @@ function promptRulesCheck(load2) {
33338
33583
  const mergeMissing = missingRules(p.merge, MERGE_RULES);
33339
33584
  if (mergeMissing.length) problems.push(`missing ${mergeMissing.join(", ")} in ${role} merge section`);
33340
33585
  }
33341
- if (problems.length) return { ...base2, status: "fail", reason: problems.join("; "), fix: "upgrade @pitboss/cli" };
33586
+ if (problems.length) return { ...base2, status: "fail", reason: problems.join("; "), fix: "upgrade @shipster/cli" };
33342
33587
  return { ...base2, status: "ok", reason: "all six playbook rules are in the shipped prompts", fix: "" };
33343
33588
  }
33344
33589
  };
@@ -33577,7 +33822,7 @@ function makeInstallProbe(cwd, repoOverride) {
33577
33822
  },
33578
33823
  async mcp() {
33579
33824
  const found = creds();
33580
- if (!found.ok) return { ok: false, reason: "no usable pitboss login for this folder" };
33825
+ if (!found.ok) return { ok: false, reason: "no usable shipster login for this folder" };
33581
33826
  const repoId = repoOverride || found.repoId || "";
33582
33827
  if (!repoId) return { ok: false, reason: "no repo id to start the MCP server with" };
33583
33828
  const entry = readEnv("DOCTOR_MCP_ENTRY") || fileURLToPath4(new URL("mcp.js", import.meta.url));
@@ -33604,7 +33849,7 @@ function parseGithubSlug(url) {
33604
33849
  }
33605
33850
  function makeRealEnv(opts) {
33606
33851
  const cwd = resolve18(opts.dir ?? process.cwd());
33607
- const exec4 = (cmd, args, o) => {
33852
+ const exec5 = (cmd, args, o) => {
33608
33853
  const r = spawnSync4(cmd, args, { cwd: o?.cwd ?? cwd, encoding: "utf8", timeout: o?.timeoutMs ?? 1e4 });
33609
33854
  return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "", code: r.status };
33610
33855
  };
@@ -33619,7 +33864,7 @@ function makeRealEnv(opts) {
33619
33864
  return res.json();
33620
33865
  }
33621
33866
  } : null;
33622
- const remote = exec4("git", ["remote", "get-url", "origin"]);
33867
+ const remote = exec5("git", ["remote", "get-url", "origin"]);
33623
33868
  return {
33624
33869
  cwd,
33625
33870
  readFile(path3) {
@@ -33629,7 +33874,7 @@ function makeRealEnv(opts) {
33629
33874
  return null;
33630
33875
  }
33631
33876
  },
33632
- exec: exec4,
33877
+ exec: exec5,
33633
33878
  api,
33634
33879
  now: () => Date.now(),
33635
33880
  repoSlug: remote.ok ? parseGithubSlug(remote.stdout) : null,
@@ -33651,14 +33896,14 @@ async function doctorCommand(opts) {
33651
33896
 
33652
33897
  // src/index.ts
33653
33898
  var program2 = new Command();
33654
- program2.name("pitboss").description("Coordinate your coding agents from the terminal").version(CLI_VERSION).option("--no-input", "Never prompt; require values via flags").hook("preAction", (thisCommand) => {
33899
+ program2.name("shipster").description("Coordinate your coding agents from the terminal").version(CLI_VERSION).option("--no-input", "Never prompt; require values via flags").addHelpText("after", "\nFormerly pitboss: the pitboss command still works. See docs/renamed-from-pitboss.md.\n").hook("preAction", (thisCommand) => {
33655
33900
  if (thisCommand.opts().input === false) setEnv("NO_INPUT", "1");
33656
33901
  });
33657
33902
  program2.command("mcp", { hidden: true }).description("Run the MCP server on stdio (used by agent config, not by hand)").allowUnknownOption().action(mcpCommand);
33658
- program2.command("join").description("Connect this machine's agents: opens a code you approve in the browser").option("--api <url>", "API base URL of your pitboss instance (defaults to the hosted one)").action(joinCommand);
33659
- program2.command("init").description("Alias for `join`, kept so existing docs keep working").option("--api <url>", "relai API base URL (defaults to the hosted one)").action(joinCommand);
33903
+ program2.command("join").description("Connect this machine's agents: opens a code you approve in the browser").option("--api <url>", "API base URL of your Shipster instance (defaults to the hosted one)").action(joinCommand);
33904
+ program2.command("init").description("Alias for `join`, kept so existing docs keep working").option("--api <url>", "API base URL of your Shipster instance (defaults to the hosted one)").action(joinCommand);
33660
33905
  program2.command("status").description("Show your agent info, online agents, task summary, and unread count").action(statusCommand);
33661
- program2.command("doctor").description("Check this install and repo for setup problems; exits 0 unless --strict and a check fails").option("--json", "Print the stable JSON report").option("--strict", "Exit 1 when any check fails (warnings never change the code)").option("--dir <path>", "Repo folder to check (defaults to the current directory)").option("--repo <id>", "Pitboss repo id to use for API checks (defaults to this folder's config)").action(doctorCommand);
33906
+ program2.command("doctor").description("Check this install and repo for setup problems; exits 0 unless --strict and a check fails").option("--json", "Print the stable JSON report").option("--strict", "Exit 1 when any check fails (warnings never change the code)").option("--dir <path>", "Repo folder to check (defaults to the current directory)").option("--repo <id>", "Shipster repo id to use for API checks (defaults to this folder's config)").action(doctorCommand);
33662
33907
  program2.command("start").description("Show your session orientation: project context, your open tasks, unread messages, open threads").action(startCommand);
33663
33908
  program2.command("watch").description("Stream live events you're subscribed to (new tasks, messages, reviews) until Ctrl-C").option("--kinds <list>", "Only show these event kinds (comma-separated, e.g. task.created,message.posted)").option("--once", "Block until the next event for this folder's agent, print it as JSON, and exit").option("--since <evt_id|now>", "Start after this event (an evt_ id), or skip the backlog (now). Overwrites the saved position").option("--status", "Print the saved position and how many events are waiting, then exit. Opens the event stream briefly, so the API marks the agent as listening").option("--dir <path>", "Folder whose agent config --once uses (defaults to the current directory)").action(watchCommand);
33664
33909
  var hook = program2.command("hook", { hidden: true }).description("Agent runtime hooks that keep the watcher running (installed by join)");
@@ -33684,7 +33929,7 @@ program2.command("inbox").description("Show unread messages").option("-r, --read
33684
33929
  program2.command("repos").description("List all repos on this server").action(reposListCommand);
33685
33930
  var agentsCmd = program2.command("agents").description("List agents in the current repo").action(agentsListCommand);
33686
33931
  agentsCmd.command("report").description("Read-only report of duplicate and stale agent rows; changes nothing").option("--repo <id>", "Limit to one repo (default: every repo you own)").option("--json", "Print the rows as JSON").action(agentsReportCommand);
33687
- program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `pitboss repo invite`").option("--token <token>", "Per-agent token (from the cloud dashboard)").option("--api <url>", "API URL (skips prompt)").option("--working-dir <path>", "Override the working directory (defaults to CWD)").option("--worker-type <type>", `Agent runtime: ${WORKER_TYPES.join(", ")} (default: claude)`).option("--name <name>", "Agent name (defaults to the invite's suggestion when not interactive)").option("--specialization <spec>", "Agent specialization: writer, reviewer, tester, architect, devops, orchestrator (defaults to the invite's suggestion)").action(loginCommand);
33932
+ program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `shipster repo invite`").option("--token <token>", "Per-agent token (from the cloud dashboard)").option("--api <url>", "API URL (skips prompt)").option("--working-dir <path>", "Override the working directory (defaults to CWD)").option("--worker-type <type>", `Agent runtime: ${WORKER_TYPES.join(", ")} (default: claude)`).option("--name <name>", "Agent name (defaults to the invite's suggestion when not interactive)").option("--specialization <spec>", "Agent specialization: writer, reviewer, tester, architect, devops, orchestrator (defaults to the invite's suggestion)").action(loginCommand);
33688
33933
  program2.command("logout").description("Release this directory's local agent claim (does not revoke the server token)").option("--all", "Sweep claims whose working directories no longer exist").action(logoutCommand);
33689
33934
  var repo = program2.command("repo").description("Repo operations");
33690
33935
  repo.command("show [id]").description("Show a repo's details (defaults to the current repo)").action(repoShowCommand);
@@ -33694,23 +33939,23 @@ repoContext.command("edit").description("Open the pinned context in $EDITOR").ac
33694
33939
  repo.command("invite").description("Create an invite code for another agent to join this repo").option("-n, --name <name>", "Suggested agent name (the new agent can override)").option("-s, --specialization <s>", "Suggested specialization").option("--role <role>", "Role this invite grants: orchestrator | worker (default worker)").option("--ttl <seconds>", "Expiry in seconds (default: 7 days)").action(repoInviteCommand);
33695
33940
  var pr = program2.command("pr").description("Pull request helpers");
33696
33941
  pr.command("remerge-check <sha>").description("Classify a merge commit's remerge diff: exit 0 conflict-only, 1 behaviour change, 2 error").option("--json", "Print the classification as JSON").action(remergeCheckCommand);
33697
- program2.command("mutate-diff").description("Mutate the lines this branch changed and run the related tests; survivors are gaps the tests do not cover. Always exits 0").option("--base <ref>", "Compare against this ref (default origin/main)").option("--json", "Print the report as JSON instead of a table").option("--markdown", "Print the report as GitHub markdown").option("--markdown-out <file>", "Also write the GitHub markdown report to this file").option("--budget <sec>", "Stop launching mutants after this many seconds (default 120)").option("--timeout <sec>", "Kill a single mutant run after this many seconds (default 60)").option("--baseline-timeout <sec>", "Kill an unmutated baseline run after this many seconds (default 300); a baseline over 60 s skips its file").option("--paths <list>", "Comma-separated git pathspecs that limit the diff").option("--out <file>", "Also write the JSON report to this file").action(mutateDiffCommand);
33942
+ program2.command("mutate-diff").description("Mutate the lines this branch changed and run the related tests; survivors are gaps the tests do not cover. Always exits 0").option("--base <ref>", "Compare against this ref (default origin/main)").option("--json", "Print the report as JSON instead of a table").option("--markdown", "Print the report as GitHub markdown").option("--markdown-out <file>", "Also write the GitHub markdown report to this file").option("--budget <sec>", "Stop launching mutants after this many seconds (default 120)").option("--total-budget <sec>", "Start no baseline or mutant after this many seconds, so a report is always written (default: no cap)").option("--timeout <sec>", "Kill a single mutant run after this many seconds (default 60)").option("--baseline-timeout <sec>", "Kill an unmutated baseline run after this many seconds (default 300); a baseline over 60 s skips its file").option("--paths <list>", "Comma-separated git pathspecs that limit the diff").option("--out <file>", "Also write the JSON report to this file").action(mutateDiffCommand);
33698
33943
  var token = program2.command("token").description("Manage your agent's API tokens");
33699
33944
  token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
33700
33945
  token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
33701
33946
  token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
33702
- 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("--architect <runtime>", "Runtime for the tier-2 architect (asked for when there is a terminal and no flag)").option("--writer <runtime>", "Runtime for the writer pool (asked for when there is a terminal and no flag)").option("--reviewer <runtime>", "Runtime for the reviewer; may match the writers, with a warning (asked for when there is a terminal and no flag)").option("--pool-max <n>", "Writer pool size (default 2)").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
33947
+ program2.command("onboard <github-url>").description("Set a GitHub repo up for Shipster: clone it, write a node spec (tier-2 architect, reviewer, writer pool) and print one device code that grants every agent. Starts nothing").option("--account <login>", "GitHub account to clone and work as, from `gh auth` (for example a personal account)").option("--dir <path>", "Parent directory for the clone, worktrees and spec (default ~/github)").option("--architect <runtime>", "Runtime for the tier-2 architect (asked for when there is a terminal and no flag)").option("--writer <runtime>", "Runtime for the writer pool (asked for when there is a terminal and no flag)").option("--reviewer <runtime>", "Runtime for the reviewer; may match the writers, with a warning (asked for when there is a terminal and no flag)").option("--pool-max <n>", "Writer pool size (default 2)").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
33703
33948
  var node = program2.command("node").description("Run this machine's agents headless and keep them running");
33704
33949
  node.command("up [spec]").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them). With no spec file, follows the desired state the dashboard holds for this enrolled node").option(
33705
33950
  "--invite <name=code>",
33706
- "Redeem a pre-minted worker invite for that spec agent (repeatable). Prefer PITBOSS_NODE_INVITE_<NAME>: a flag puts the code on argv, which ps and shell history can see",
33951
+ "Redeem a pre-minted worker invite for that spec agent (repeatable). Prefer SHIPSTER_NODE_INVITE_<NAME>: a flag puts the code on argv, which ps and shell history can see",
33707
33952
  (value, acc) => {
33708
33953
  acc.push(value);
33709
33954
  return acc;
33710
33955
  },
33711
33956
  []
33712
33957
  ).option("--adopt-new", "Register a new agent even when one with the same name already exists in the repo. Without it a name clash refuses that agent, since its credentials live in another config dir").option("--unsafe-shared-user", "Local development only: run owner-pool seats that have no runAs as this node's own user, which can read anything that user can, including ~/.ssh (SSH keys cannot be detected). Logs a warning on every start, and the node refuses to start while it holds GH_TOKEN or GITHUB_TOKEN, while `gh auth token` succeeds, while a git credential.helper is configured, or while ~/.git-credentials exists. Also PITBOSS_UNSAFE_SHARED_USER=1").action(nodeUpCommand);
33713
- node.command("enroll [code]").description("Turn a dashboard enrollment code into this machine's node credential. Prefer PITBOSS_NODE_ENROLL_CODE: an argument is visible to ps and shell history").option("--api <url>", "API url (default: the one `pitboss login` saved)").option("--force", "Replace an existing node credential").option("--root <dir>", "Directory every desired worker clone lives under (default <config>/node/work); created 0700, never / or your home directory").option("--allow-env <names>", "Comma-separated environment variable NAMES a dashboard-added worker may be granted (default: none)").option("--allow-host <hosts>", "Comma-separated hosts, besides github.com, this machine's GH_TOKEN may be sent to when cloning (default: none)").option("--allow-run-as <users>", "Comma-separated system users a dashboard-added worker may run as, Linux only (default: none, so workers run as the node user)").action(nodeEnrollCommand);
33958
+ node.command("enroll [code]").description("Turn a dashboard enrollment code into this machine's node credential. Prefer SHIPSTER_NODE_ENROLL_CODE: an argument is visible to ps and shell history").option("--api <url>", "API url (default: the one `shipster login` saved)").option("--force", "Replace an existing node credential").option("--root <dir>", "Directory every desired worker clone lives under (default <config>/node/work); created 0700, never / or your home directory").option("--allow-env <names>", "Comma-separated environment variable NAMES a dashboard-added worker may be granted (default: none)").option("--allow-host <hosts>", "Comma-separated hosts, besides github.com, this machine's GH_TOKEN may be sent to when cloning (default: none)").option("--allow-run-as <users>", "Comma-separated system users a dashboard-added worker may run as, Linux only (default: none, so workers run as the node user)").action(nodeEnrollCommand);
33714
33959
  node.command("status").description("Show the agents the last `node up` on this machine is supervising").option("--json", "Print machine-readable output").action(nodeStatusCommand);
33715
33960
  node.command("check <spec>").description("Verify the runAs file layout (owners, modes, users, capabilities) with stat only. Exit 0 ok, 1 violation, 2 spec unreadable").option("--root <dir>", "Work root: must be traversable and not listable by workers").option("--caps-from <file>", "`systemctl show pitboss-node` output to check the unit's capability sets (read from /proc/self/status inside the unit)").action(nodeCheckCommand);
33716
33961
  node.command("_prepare <seat>", { hidden: true }).description("Internal: write a runAs seat's MCP config as that seat's user, from one JSON document on stdin").action(nodePrepareCommand);