@pitboss/cli 0.4.4 → 0.4.5

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
@@ -17463,28 +17463,28 @@ var RemoveFileError = class extends Error {
17463
17463
 
17464
17464
  // ../../node_modules/.pnpm/@inquirer+external-editor@3.0.4_@types+node@22.19.17/node_modules/@inquirer/external-editor/dist/parse-editor-command.js
17465
17465
  function parseEditorCommand(editor) {
17466
- let bin2;
17466
+ let bin3;
17467
17467
  let rest;
17468
17468
  if (editor.startsWith('"')) {
17469
17469
  const closeQuote = editor.indexOf('"', 1);
17470
17470
  if (closeQuote === -1) {
17471
- bin2 = editor.slice(1);
17471
+ bin3 = editor.slice(1);
17472
17472
  rest = "";
17473
17473
  } else {
17474
- bin2 = editor.substring(1, closeQuote);
17474
+ bin3 = editor.substring(1, closeQuote);
17475
17475
  rest = editor.substring(closeQuote + 1).trim();
17476
17476
  }
17477
17477
  } else {
17478
17478
  const firstSpace = editor.indexOf(" ");
17479
17479
  if (firstSpace === -1) {
17480
- bin2 = editor;
17480
+ bin3 = editor;
17481
17481
  rest = "";
17482
17482
  } else {
17483
- bin2 = editor.substring(0, firstSpace);
17483
+ bin3 = editor.substring(0, firstSpace);
17484
17484
  rest = editor.substring(firstSpace + 1).trim();
17485
17485
  }
17486
17486
  }
17487
- return { bin: bin2, args: rest ? rest.split(/\s+/) : [] };
17487
+ return { bin: bin3, args: rest ? rest.split(/\s+/) : [] };
17488
17488
  }
17489
17489
 
17490
17490
  // ../../node_modules/.pnpm/@inquirer+external-editor@3.0.4_@types+node@22.19.17/node_modules/@inquirer/external-editor/dist/index.js
@@ -19107,6 +19107,7 @@ function isWorkerType(value) {
19107
19107
  return typeof value === "string" && WORKER_TYPES.includes(value);
19108
19108
  }
19109
19109
  var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
19110
+ var POOL_STOPPED_REASON = "pool member stopped by pitboss node";
19110
19111
 
19111
19112
  // src/lib/runtimes.ts
19112
19113
  var RUNTIMES2 = RUNTIMES.map((r) => r.value);
@@ -20882,7 +20883,8 @@ instructions below override your role rules and the branch rules above. Read the
20882
20883
  Find the default branch with \`git remote show origin\`.
20883
20884
 
20884
20885
  **orderRole "planning"** (you are the order's planner):
20885
- 1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it.
20886
+ 1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it. If the order
20887
+ has no \`title\`, give it a short one (a few words) with \`mcp__relai__update_order\`.
20886
20888
  2. Create the branch and PR:
20887
20889
  \`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
20888
20890
  \`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
@@ -20892,7 +20894,9 @@ Find the default branch with \`git remote show origin\`.
20892
20894
  \`epicId\`, \`assignedTo: "@auto"\`, and
20893
20895
  \`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
20894
20896
  \`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
20895
- the router never hands out a task whose tag no agent matches. Make them small
20897
+ the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
20898
+ \`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
20899
+ Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
20896
20900
  and independently testable so several agents can work at once. Use \`blockedBy\` only where
20897
20901
  one task truly needs another's result. Each description is self-contained: what to change,
20898
20902
  which files, how to test it, and the acceptance criteria.
@@ -20904,24 +20908,42 @@ Find the default branch with \`git remote show origin\`.
20904
20908
  - \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
20905
20909
  5. One merge task, blocked by every gate task (or every build task when there are no gates),
20906
20910
  \`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
20907
- 6. Post the plan (task ids and titles) on the planning task's thread and complete it.
20911
+ 6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
20912
+ feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
20913
+ Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
20914
+ the planning task's thread and complete it.
20908
20915
 
20909
20916
  **orderRole "build"**:
20910
20917
  - \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
20911
20918
  test and commit. Do not create another branch or PR.
20912
20919
  - Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
20913
20920
  repeating up to 5 times if the push is rejected. Never force-push.
20921
+ - If you drop or change any part of what the goal asked for, even for a good reason, record each one
20922
+ with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
20923
+ "reason": "<why>" }]\` and say so in your handoff. The owner decides whether to accept it.
20914
20924
  - Hand off on the task's thread with the commit sha and what you tested, then complete.
20915
20925
 
20916
20926
  **orderRole "gate"**:
20917
20927
  - Check every build task in the order's Epic is completed, then post the PR link on the task's
20918
- thread and complete it. The gate decides from there: the owner approves a \`human_approval\`
20928
+ thread, listing every \`metadata.deviations\` entry from the build tasks, and complete it. The gate decides from there: the owner approves a \`human_approval\`
20919
20929
  gate on the dashboard and the named reviewer decides a \`reviewer_agent\` gate.
20930
+ - As the reviewer of an order, check each thing the goal asked for. Anything dropped or changed is
20931
+ the owner's call, not yours: if it is recorded in \`metadata.deviations\`, mention it and judge
20932
+ the rest; if it is not recorded, reject and name it so the builder records it. The owner decides
20933
+ on deviations in the approval gate.
20920
20934
 
20921
20935
  **orderRole "merge"**:
20922
20936
  1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
20937
+ Before merging, replace the PR body with a run report built from
20938
+ \`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
20939
+ (planning, build, gates, merge), each task with its assignee, status, and created and
20940
+ completed times, the number of distinct agents that worked on it, the total elapsed time
20941
+ from the planning task's creation, and the CI checks that ran. Use \`gh pr edit <prUrl> --body-file <file>\`.
20923
20942
  2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
20924
- gh pr view --json headRefOid>\`, post the merge commit on the thread, and complete.
20943
+ gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
20944
+ the merge commit and time, and recompute the elapsed time to the merge, with
20945
+ \`gh pr edit <prUrl> --body-file <file>\` (a merged PR still accepts an edit). Post the merge commit
20946
+ on the thread, and complete.
20925
20947
  3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
20926
20948
  After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
20927
20949
  function buildPrompt(config) {
@@ -21076,7 +21098,7 @@ function mcpServerLaunch(here = fileURLToPath2(import.meta.url), exists = exists
21076
21098
  if (exists(bundled)) return { command: execPath, args: [bundled] };
21077
21099
  return { command: execPath, args: [mcpServerTsxCli(), mcpServerEntry()] };
21078
21100
  }
21079
- function writeMcpConfig(config) {
21101
+ function writeMcpConfig(config, prefix = "claude-worker") {
21080
21102
  const { command, args } = mcpServerLaunch();
21081
21103
  const mcpConfig = {
21082
21104
  mcpServers: {
@@ -21092,7 +21114,7 @@ function writeMcpConfig(config) {
21092
21114
  }
21093
21115
  }
21094
21116
  };
21095
- const path3 = join12(tmpdir(), `claude-worker-${config.agentId}-${Date.now()}.json`);
21117
+ const path3 = join12(tmpdir(), `${prefix}-${config.agentId}-${Date.now()}.json`);
21096
21118
  writeFileSync6(path3, JSON.stringify(mcpConfig), { mode: 384 });
21097
21119
  return path3;
21098
21120
  }
@@ -21331,9 +21353,111 @@ var claudeAdapter = {
21331
21353
  classifyError: classifySessionError
21332
21354
  };
21333
21355
 
21356
+ // ../claude-worker/src/adapters/copilot.ts
21357
+ import { spawn as spawn4 } from "child_process";
21358
+ import { unlinkSync as unlinkSync2 } from "fs";
21359
+ var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
21360
+ var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
21361
+ var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
21362
+ function copilotArgs(repoPath, prompt, mcpConfigPath) {
21363
+ return [
21364
+ "-p",
21365
+ prompt,
21366
+ "--allow-all-tools",
21367
+ "--output-format",
21368
+ "json",
21369
+ "--no-color",
21370
+ "-C",
21371
+ repoPath,
21372
+ "--additional-mcp-config",
21373
+ `@${mcpConfigPath}`,
21374
+ "--disable-builtin-mcps"
21375
+ ];
21376
+ }
21377
+ function readCopilotEvent(line) {
21378
+ let event;
21379
+ try {
21380
+ event = JSON.parse(line);
21381
+ } catch {
21382
+ return {};
21383
+ }
21384
+ const type = typeof event.type === "string" ? event.type : "";
21385
+ const data = event.data ?? {};
21386
+ const message = typeof data.message === "string" ? data.message : "";
21387
+ if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
21388
+ if (type === "session.error") return { error: message || "copilot session error" };
21389
+ if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
21390
+ if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
21391
+ return {};
21392
+ }
21393
+ async function runCopilotSession(config) {
21394
+ const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
21395
+ const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
21396
+ console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
21397
+ return new Promise((resolve8, reject) => {
21398
+ const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21399
+ console.log(`[copilot-worker] Process PID: ${proc.pid}`);
21400
+ const toolsUsed = [];
21401
+ let buffer = "";
21402
+ let sessionError = "";
21403
+ let stderrOutput = "";
21404
+ proc.stdout.on("data", (chunk) => {
21405
+ buffer += chunk.toString();
21406
+ const lines = buffer.split("\n");
21407
+ buffer = lines.pop() ?? "";
21408
+ for (const line of lines) {
21409
+ const { tool, error } = readCopilotEvent(line);
21410
+ if (tool) {
21411
+ toolsUsed.push(tool);
21412
+ process.stdout.write(` \u2699 ${tool}
21413
+ `);
21414
+ }
21415
+ if (error) {
21416
+ sessionError = error;
21417
+ console.error(`[copilot-worker] Session failed: ${error}`);
21418
+ }
21419
+ }
21420
+ });
21421
+ proc.stderr.on("data", (chunk) => {
21422
+ stderrOutput += chunk.toString();
21423
+ });
21424
+ proc.on("close", (code, signal) => {
21425
+ try {
21426
+ unlinkSync2(mcpConfigPath);
21427
+ } catch {
21428
+ }
21429
+ console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
21430
+ if (sessionError || signal || code !== 0 && code !== null) {
21431
+ const detail = (sessionError || stderrOutput).trim();
21432
+ reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21433
+ } else {
21434
+ resolve8([...new Set(toolsUsed)]);
21435
+ }
21436
+ });
21437
+ proc.on("error", (err) => {
21438
+ try {
21439
+ unlinkSync2(mcpConfigPath);
21440
+ } catch {
21441
+ }
21442
+ reject(err);
21443
+ });
21444
+ });
21445
+ }
21446
+ var copilotAdapter = {
21447
+ runtime: "copilot",
21448
+ prepare() {
21449
+ },
21450
+ runSession: runCopilotSession,
21451
+ classifyError: (text) => {
21452
+ if (POLICY_BLOCKS.some((re) => re.test(text))) return "credentials";
21453
+ if (QUOTA.some((re) => re.test(text))) return "quota";
21454
+ return classifySessionError(text);
21455
+ }
21456
+ };
21457
+
21334
21458
  // ../claude-worker/src/adapters/index.ts
21335
- var RUNTIMES3 = ["claude", "cursor"];
21336
- var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
21459
+ var RUNTIMES3 = ["claude", "cursor", "copilot"];
21460
+ var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
21337
21461
  function getAdapter(runtime) {
21338
21462
  const adapter = ADAPTERS[runtime];
21339
21463
  if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
@@ -21868,6 +21992,7 @@ function describeState(state, alive = isAlive) {
21868
21992
  max: p.max,
21869
21993
  ready: p.ready,
21870
21994
  checkedAt: p.checkedAt,
21995
+ desiredMax: p.desiredMax ?? null,
21871
21996
  error: p.error ?? null,
21872
21997
  running: p.members.filter((m) => byName.get(m)?.status === "running").length
21873
21998
  }));
@@ -21965,6 +22090,11 @@ function poolTimings(env2 = process.env) {
21965
22090
  idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
21966
22091
  };
21967
22092
  }
22093
+ function effectiveBounds(spec, desired) {
22094
+ if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
22095
+ const max = Math.min(desired.max, spec.max);
22096
+ return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
22097
+ }
21968
22098
  function reconcilePool({ pending, members, min, max, idleStopMs }) {
21969
22099
  const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
21970
22100
  const running = members.filter((m) => m.running);
@@ -22150,8 +22280,8 @@ function prepareAgent(agent, creds, mcp) {
22150
22280
  writeMergedJson(target, (existing) => mergeMcpServer(existing, name, entry), root);
22151
22281
  excludeIfUntracked(root, target);
22152
22282
  },
22153
- run(bin2, args, cwd) {
22154
- execFileSync7(bin2, args, { cwd, stdio: "ignore" });
22283
+ run(bin3, args, cwd) {
22284
+ execFileSync7(bin3, args, { cwd, stdio: "ignore" });
22155
22285
  }
22156
22286
  });
22157
22287
  }
@@ -22180,8 +22310,14 @@ async function fetchTasks(creds, query) {
22180
22310
  if (res.status !== 200 || !Array.isArray(res.payload.data)) throw new Error(`GET /tasks?${query} returned HTTP ${res.status}`);
22181
22311
  return res.payload.data;
22182
22312
  }
22313
+ async function fetchPoolSize(creds, name) {
22314
+ const res = await getJson(`${creds.api}/repos/${encodeURIComponent(creds.repoId)}/pools`, creds.token);
22315
+ if (res.status !== 200 || !Array.isArray(res.payload.data)) return void 0;
22316
+ const row = res.payload.data.find((p) => p.name === name);
22317
+ return row ? { min: row.min, max: row.max } : void 0;
22318
+ }
22183
22319
  async function setAvailable(creds, available) {
22184
- const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason: "pool member stopped by pitboss node" };
22320
+ const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason: POOL_STOPPED_REASON };
22185
22321
  const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/unavailable`, {
22186
22322
  method: "PUT",
22187
22323
  headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
@@ -22189,6 +22325,14 @@ async function setAvailable(creds, available) {
22189
22325
  });
22190
22326
  if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/unavailable returned HTTP ${res.status}`);
22191
22327
  }
22328
+ async function reportScale(creds, event) {
22329
+ const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/pool-events`, {
22330
+ method: "POST",
22331
+ headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
22332
+ body: JSON.stringify(event)
22333
+ });
22334
+ if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
22335
+ }
22192
22336
  function startPoolLoop(run3, sup, record) {
22193
22337
  const { pool, members } = run3;
22194
22338
  const { reconcileMs, idleStopMs } = poolTimings();
@@ -22205,6 +22349,14 @@ function startPoolLoop(run3, sup, record) {
22205
22349
  log(`could not mark ${member.agent.name} unavailable: ${err instanceof Error ? err.message : String(err)}`);
22206
22350
  }
22207
22351
  };
22352
+ const report = async (member, action, reason, ready) => {
22353
+ const running = members.filter((m) => sup.isActive(m.agent.name)).length;
22354
+ try {
22355
+ await reportScale(member.creds, { pool: pool.pool, action, reason, ready, running, max: pool.max });
22356
+ } catch (err) {
22357
+ log(`could not report ${action} ${member.agent.name}: ${err instanceof Error ? err.message : String(err)}`);
22358
+ }
22359
+ };
22208
22360
  const tick = async () => {
22209
22361
  if (busy) return;
22210
22362
  busy = true;
@@ -22224,7 +22376,12 @@ function startPoolLoop(run3, sup, record) {
22224
22376
  if (counts.assigned + counts.inProgress > 0) lastBusy.set(agent.name, now);
22225
22377
  return { name: agent.name, running: sup.isActive(agent.name), ...counts, idleMs: now - (lastBusy.get(agent.name) ?? now) };
22226
22378
  });
22227
- const decision = reconcilePool({ pending: summary.pending, members: views, min: pool.min, max: pool.max, idleStopMs });
22379
+ const bounds = effectiveBounds(pool, await fetchPoolSize(reader.creds, pool.pool));
22380
+ if (bounds.max !== state.max || bounds.min !== state.min) {
22381
+ log(`size min ${bounds.min} max ${bounds.max}${bounds.capped ? ` (API asks for ${bounds.desiredMax}, capped at the ${pool.max} provisioned members)` : ""}`);
22382
+ }
22383
+ Object.assign(state, { min: bounds.min, max: bounds.max, desiredMax: bounds.desiredMax });
22384
+ const decision = reconcilePool({ pending: summary.pending, members: views, min: bounds.min, max: bounds.max, idleStopMs });
22228
22385
  for (const name of decision.start) {
22229
22386
  const member = members.find((m) => m.agent.name === name);
22230
22387
  try {
@@ -22238,11 +22395,14 @@ function startPoolLoop(run3, sup, record) {
22238
22395
  lastBusy.set(name, Date.now());
22239
22396
  sup.startAgent(name);
22240
22397
  log(`started ${name} (ready ${summary.ready})`);
22398
+ await report(member, "started", `ready ${summary.ready}`, summary.ready);
22241
22399
  }
22242
22400
  for (const name of decision.stop) {
22401
+ const member = members.find((m) => m.agent.name === name);
22243
22402
  await sup.stopAgent(name);
22244
- await markStopped(members.find((m) => m.agent.name === name));
22403
+ await markStopped(member);
22245
22404
  log(`stopped ${name} after ${Math.round(idleStopMs / 1e3)}s idle`);
22405
+ await report(member, "stopped", `idle ${Math.round(idleStopMs / 1e3)}s`, summary.ready);
22246
22406
  }
22247
22407
  Object.assign(state, { ready: summary.ready, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: void 0 });
22248
22408
  } catch (err) {
@@ -22417,7 +22577,7 @@ function nodeStatusCommand(opts) {
22417
22577
  }
22418
22578
  for (const p of pools) {
22419
22579
  const ready = p.ready === null ? "-" : String(p.ready);
22420
- console.log(` pool ${p.name.padEnd(11)} running ${p.running}/${p.max} (min ${p.min}) ready ${ready} members ${p.members.join(", ")}${p.error ? source_default.red(` ${p.error}`) : ""}`);
22580
+ console.log(` pool ${p.name.padEnd(11)} running ${p.running}/${p.max} (min ${p.min})${p.desiredMax !== null && p.desiredMax > p.max ? source_default.yellow(` desired ${p.desiredMax}, capped`) : ""} ready ${ready} members ${p.members.join(", ")}${p.error ? source_default.red(` ${p.error}`) : ""}`);
22421
22581
  }
22422
22582
  }
22423
22583
 
@@ -275,7 +275,8 @@ instructions below override your role rules and the branch rules above. Read the
275
275
  Find the default branch with \`git remote show origin\`.
276
276
 
277
277
  **orderRole "planning"** (you are the order's planner):
278
- 1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it.
278
+ 1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it. If the order
279
+ has no \`title\`, give it a short one (a few words) with \`mcp__relai__update_order\`.
279
280
  2. Create the branch and PR:
280
281
  \`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
281
282
  \`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
@@ -285,7 +286,9 @@ Find the default branch with \`git remote show origin\`.
285
286
  \`epicId\`, \`assignedTo: "@auto"\`, and
286
287
  \`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
287
288
  \`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
288
- the router never hands out a task whose tag no agent matches. Make them small
289
+ the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
290
+ \`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
291
+ Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
289
292
  and independently testable so several agents can work at once. Use \`blockedBy\` only where
290
293
  one task truly needs another's result. Each description is self-contained: what to change,
291
294
  which files, how to test it, and the acceptance criteria.
@@ -297,24 +300,42 @@ Find the default branch with \`git remote show origin\`.
297
300
  - \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
298
301
  5. One merge task, blocked by every gate task (or every build task when there are no gates),
299
302
  \`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
300
- 6. Post the plan (task ids and titles) on the planning task's thread and complete it.
303
+ 6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
304
+ feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
305
+ Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
306
+ the planning task's thread and complete it.
301
307
 
302
308
  **orderRole "build"**:
303
309
  - \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
304
310
  test and commit. Do not create another branch or PR.
305
311
  - Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
306
312
  repeating up to 5 times if the push is rejected. Never force-push.
313
+ - If you drop or change any part of what the goal asked for, even for a good reason, record each one
314
+ with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
315
+ "reason": "<why>" }]\` and say so in your handoff. The owner decides whether to accept it.
307
316
  - Hand off on the task's thread with the commit sha and what you tested, then complete.
308
317
 
309
318
  **orderRole "gate"**:
310
319
  - Check every build task in the order's Epic is completed, then post the PR link on the task's
311
- thread and complete it. The gate decides from there: the owner approves a \`human_approval\`
320
+ thread, listing every \`metadata.deviations\` entry from the build tasks, and complete it. The gate decides from there: the owner approves a \`human_approval\`
312
321
  gate on the dashboard and the named reviewer decides a \`reviewer_agent\` gate.
322
+ - As the reviewer of an order, check each thing the goal asked for. Anything dropped or changed is
323
+ the owner's call, not yours: if it is recorded in \`metadata.deviations\`, mention it and judge
324
+ the rest; if it is not recorded, reject and name it so the builder records it. The owner decides
325
+ on deviations in the approval gate.
313
326
 
314
327
  **orderRole "merge"**:
315
328
  1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
329
+ Before merging, replace the PR body with a run report built from
330
+ \`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
331
+ (planning, build, gates, merge), each task with its assignee, status, and created and
332
+ completed times, the number of distinct agents that worked on it, the total elapsed time
333
+ from the planning task's creation, and the CI checks that ran. Use \`gh pr edit <prUrl> --body-file <file>\`.
316
334
  2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
317
- gh pr view --json headRefOid>\`, post the merge commit on the thread, and complete.
335
+ gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
336
+ the merge commit and time, and recompute the elapsed time to the merge, with
337
+ \`gh pr edit <prUrl> --body-file <file>\` (a merged PR still accepts an edit). Post the merge commit
338
+ on the thread, and complete.
318
339
  3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
319
340
  After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
320
341
  function buildPrompt(config2) {
@@ -469,7 +490,7 @@ function mcpServerLaunch(here = fileURLToPath(import.meta.url), exists = existsS
469
490
  if (exists(bundled)) return { command: execPath, args: [bundled] };
470
491
  return { command: execPath, args: [mcpServerTsxCli(), mcpServerEntry()] };
471
492
  }
472
- function writeMcpConfig(config2) {
493
+ function writeMcpConfig(config2, prefix = "claude-worker") {
473
494
  const { command, args } = mcpServerLaunch();
474
495
  const mcpConfig = {
475
496
  mcpServers: {
@@ -485,7 +506,7 @@ function writeMcpConfig(config2) {
485
506
  }
486
507
  }
487
508
  };
488
- const path = join(tmpdir(), `claude-worker-${config2.agentId}-${Date.now()}.json`);
509
+ const path = join(tmpdir(), `${prefix}-${config2.agentId}-${Date.now()}.json`);
489
510
  writeFileSync(path, JSON.stringify(mcpConfig), { mode: 384 });
490
511
  return path;
491
512
  }
@@ -724,9 +745,111 @@ var claudeAdapter = {
724
745
  classifyError: classifySessionError
725
746
  };
726
747
 
748
+ // ../claude-worker/src/adapters/copilot.ts
749
+ import { spawn as spawn3 } from "child_process";
750
+ import { unlinkSync as unlinkSync2 } from "fs";
751
+ var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
752
+ var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
753
+ var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
754
+ function copilotArgs(repoPath, prompt, mcpConfigPath) {
755
+ return [
756
+ "-p",
757
+ prompt,
758
+ "--allow-all-tools",
759
+ "--output-format",
760
+ "json",
761
+ "--no-color",
762
+ "-C",
763
+ repoPath,
764
+ "--additional-mcp-config",
765
+ `@${mcpConfigPath}`,
766
+ "--disable-builtin-mcps"
767
+ ];
768
+ }
769
+ function readCopilotEvent(line) {
770
+ let event;
771
+ try {
772
+ event = JSON.parse(line);
773
+ } catch {
774
+ return {};
775
+ }
776
+ const type = typeof event.type === "string" ? event.type : "";
777
+ const data = event.data ?? {};
778
+ const message = typeof data.message === "string" ? data.message : "";
779
+ if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
780
+ if (type === "session.error") return { error: message || "copilot session error" };
781
+ if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
782
+ if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
783
+ return {};
784
+ }
785
+ async function runCopilotSession(config2) {
786
+ const mcpConfigPath = writeMcpConfig(config2, "copilot-worker");
787
+ const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath);
788
+ console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
789
+ return new Promise((resolve, reject) => {
790
+ const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
791
+ console.log(`[copilot-worker] Process PID: ${proc.pid}`);
792
+ const toolsUsed = [];
793
+ let buffer = "";
794
+ let sessionError = "";
795
+ let stderrOutput = "";
796
+ proc.stdout.on("data", (chunk) => {
797
+ buffer += chunk.toString();
798
+ const lines = buffer.split("\n");
799
+ buffer = lines.pop() ?? "";
800
+ for (const line of lines) {
801
+ const { tool, error } = readCopilotEvent(line);
802
+ if (tool) {
803
+ toolsUsed.push(tool);
804
+ process.stdout.write(` \u2699 ${tool}
805
+ `);
806
+ }
807
+ if (error) {
808
+ sessionError = error;
809
+ console.error(`[copilot-worker] Session failed: ${error}`);
810
+ }
811
+ }
812
+ });
813
+ proc.stderr.on("data", (chunk) => {
814
+ stderrOutput += chunk.toString();
815
+ });
816
+ proc.on("close", (code, signal) => {
817
+ try {
818
+ unlinkSync2(mcpConfigPath);
819
+ } catch {
820
+ }
821
+ console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
822
+ if (sessionError || signal || code !== 0 && code !== null) {
823
+ const detail = (sessionError || stderrOutput).trim();
824
+ reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
825
+ } else {
826
+ resolve([...new Set(toolsUsed)]);
827
+ }
828
+ });
829
+ proc.on("error", (err) => {
830
+ try {
831
+ unlinkSync2(mcpConfigPath);
832
+ } catch {
833
+ }
834
+ reject(err);
835
+ });
836
+ });
837
+ }
838
+ var copilotAdapter = {
839
+ runtime: "copilot",
840
+ prepare() {
841
+ },
842
+ runSession: runCopilotSession,
843
+ classifyError: (text) => {
844
+ if (POLICY_BLOCKS.some((re) => re.test(text))) return "credentials";
845
+ if (QUOTA.some((re) => re.test(text))) return "quota";
846
+ return classifySessionError(text);
847
+ }
848
+ };
849
+
727
850
  // ../claude-worker/src/adapters/index.ts
728
- var RUNTIMES = ["claude", "cursor"];
729
- var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
851
+ var RUNTIMES = ["claude", "cursor", "copilot"];
852
+ var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
730
853
  function getAdapter(runtime) {
731
854
  const adapter = ADAPTERS[runtime];
732
855
  if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
package/dist/mcp.js CHANGED
@@ -12433,6 +12433,12 @@ var ApiClient = class {
12433
12433
  archiveThread(threadId) {
12434
12434
  return this.request("PUT", `/threads/${threadId}/archive`, {});
12435
12435
  }
12436
+ listPools(repoId) {
12437
+ return this.request("GET", `/repos/${encodeURIComponent(repoId)}/pools`);
12438
+ }
12439
+ setPoolSize(repoId, name, body) {
12440
+ return this.request("PUT", `/repos/${encodeURIComponent(repoId)}/pools/${encodeURIComponent(name)}`, body);
12441
+ }
12436
12442
  createOrder(body) {
12437
12443
  return this.request("POST", "/orders", body);
12438
12444
  }
@@ -12531,6 +12537,7 @@ function buildCreateOrderTool(client, repoId, opts) {
12531
12537
  inputSchema: external_exports.object({
12532
12538
  repoId: external_exports.string().optional().describe(opts.ownerScoped ? "Repo to file the order in: any repo your owner owns. Defaults to your home repo." : "Repo to file the order in. Defaults to this agent's home repo."),
12533
12539
  feature: external_exports.string().min(1).describe("What to build, in the requester's words."),
12540
+ title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal. The planner writes one when omitted."),
12534
12541
  acceptance: external_exports.array(external_exports.enum(["ci_passed", "human_approval", "reviewer_agent"])).optional().describe("Gates that must all pass. Defaults to human_approval."),
12535
12542
  budget: external_exports.record(external_exports.unknown()).optional().describe("Optional ceilings. Stored only; not enforced.")
12536
12543
  }),
@@ -12539,6 +12546,7 @@ function buildCreateOrderTool(client, repoId, opts) {
12539
12546
  const created = await client.createOrder({
12540
12547
  repoId: targetRepo(input),
12541
12548
  feature: input.feature,
12549
+ ...input.title ? { title: input.title } : {},
12542
12550
  acceptance: input.acceptance,
12543
12551
  budget: input.budget
12544
12552
  });
@@ -12553,15 +12561,17 @@ function buildCreateOrderTool(client, repoId, opts) {
12553
12561
  function buildUpdateOrderTool(client) {
12554
12562
  return {
12555
12563
  name: "update_order",
12556
- description: "Record the order's branch and PR, or cancel it. For the repo orchestrator and the order's planner. " + ORDER_GATE_NOTE,
12564
+ description: "Record the order's short title, branch and PR, or cancel it. For the repo orchestrator and the order's planner. " + ORDER_GATE_NOTE,
12557
12565
  inputSchema: external_exports.object({
12558
12566
  id: external_exports.string().describe("The order id (ord_\u2026)."),
12567
+ title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal."),
12559
12568
  branch: external_exports.string().min(1).optional().describe("The order's branch."),
12560
12569
  prUrl: external_exports.string().url().optional().describe("The order's pull request URL."),
12561
12570
  cancel: external_exports.boolean().optional().describe("Set true to cancel the order.")
12562
12571
  }),
12563
12572
  handler: async (input) => {
12564
12573
  const body = {};
12574
+ if (input.title !== void 0) body.title = input.title;
12565
12575
  if (input.branch !== void 0) body.branch = input.branch;
12566
12576
  if (input.prUrl !== void 0) body.prUrl = input.prUrl;
12567
12577
  if (input.cancel) body.status = "cancelled";
@@ -12574,6 +12584,46 @@ function buildUpdateOrderTool(client) {
12574
12584
  }
12575
12585
  };
12576
12586
  }
12587
+ var POOL_NOTE = "A pool is a set of pre-approved workers that pitboss node starts and stops to match the ready work. The size set here is a target: the node never runs more members than it provisioned for the pool.";
12588
+ function buildListPoolsTool(client, repoId, opts) {
12589
+ const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
12590
+ return {
12591
+ name: "list_pools",
12592
+ description: "List the worker pool sizes set for a repo. " + POOL_NOTE,
12593
+ inputSchema: external_exports.object({
12594
+ repoId: external_exports.string().optional().describe("Repo to read. Defaults to this agent's home repo.")
12595
+ }),
12596
+ handler: async (input) => {
12597
+ try {
12598
+ const data = await client.listPools(targetRepo(input));
12599
+ return { content: [{ type: "text", text: JSON.stringify({ data }, null, 2) }] };
12600
+ } catch (err) {
12601
+ return mcpFail(err);
12602
+ }
12603
+ }
12604
+ };
12605
+ }
12606
+ function buildSetPoolSizeTool(client, repoId, opts) {
12607
+ const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
12608
+ return {
12609
+ name: "set_pool_size",
12610
+ description: "Set how many workers a repo's pool may run (max) and keep warm (min). " + POOL_NOTE + (opts.ownerScoped ? " As the owner's agent you may pass repoId for any repo your owner owns." : ""),
12611
+ inputSchema: external_exports.object({
12612
+ repoId: external_exports.string().optional().describe(opts.ownerScoped ? "Repo the pool belongs to. Defaults to your home repo." : "Ignored; the pool is in this agent's repo."),
12613
+ pool: external_exports.string().min(1).describe("The pool name from the node spec, e.g. 'ni-pool'."),
12614
+ min: external_exports.number().int().min(0).max(32).optional().describe("Members to keep running when idle. Defaults to 0."),
12615
+ max: external_exports.number().int().min(1).max(32).describe("Most members to run at once.")
12616
+ }),
12617
+ handler: async (input) => {
12618
+ try {
12619
+ const row = await client.setPoolSize(targetRepo(input), input.pool, { min: input.min ?? 0, max: input.max });
12620
+ return { content: [{ type: "text", text: JSON.stringify(row, null, 2) }] };
12621
+ } catch (err) {
12622
+ return mcpFail(err);
12623
+ }
12624
+ }
12625
+ };
12626
+ }
12577
12627
  function resolveAgentRef(candidates, ref, scope) {
12578
12628
  const available = candidates.length ? ` Available: ${candidates.map((a) => `${a.name} (${a.id})`).join(", ")}` : ` ${scope} has no agents.`;
12579
12629
  if (ref.startsWith("agent_")) {
@@ -13169,6 +13219,8 @@ ${version.body}`
13169
13219
  ...buildOrderReadTools(client, repoId, opts),
13170
13220
  ...opts.ownerScoped || opts.role === "orchestrator" ? [buildCreateOrderTool(client, repoId, opts)] : [],
13171
13221
  ...opts.role === "orchestrator" || (opts.tier ?? 0) >= 2 ? [buildUpdateOrderTool(client)] : [],
13222
+ buildListPoolsTool(client, repoId, opts),
13223
+ ...opts.ownerScoped || opts.role === "orchestrator" ? [buildSetPoolSizeTool(client, repoId, opts)] : [],
13172
13224
  // Additive, never a swap: an owner-scoped agent is still an agent and needs
13173
13225
  // everything above. buildOperatorTools assumes no agent identity, and its
13174
13226
  // create_task bypasses propose/commit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitboss/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [