@pitboss/cli 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,14 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
35
35
  human. If you need the answer before you can continue, block the task with
36
36
  \`blockedThreadId\` set to the thread the DM returned; you will be resumed when they answer.
37
37
 
38
+ **A blocker travels up the chain.** When another agent's task blocks, the repo's orchestrator
39
+ and the planner of that task's goal are told, as well as the owner. If that is you, read the
40
+ question on its \`blockedThreadId\`. When the plan, the goal, its issue or its PR already answers
41
+ it, reply there with the answer and where it comes from. As the planner, if the block shows the
42
+ plan is wrong (a missing dependency, a task too big), fix the plan. Never answer for the owner on
43
+ deletes, scope changes, approvals or publishing; those wait for the owner or their delegate.
44
+ Otherwise do nothing: the owner has already been told.
45
+
38
46
  2. Call \`mcp__relai__session_start\` for orientation. It is an **index, not an archive**: lists are
39
47
  capped and long bodies are clipped. Anything marked \`truncated: true\`, and anything named under
40
48
  \`notShown\`, is a pointer: fetch the real thing with the tool it names before you rely on it,
@@ -51,7 +59,8 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
51
59
  a one-line status on the task thread naming what you are checking) when you begin, then
52
60
  run the tests, compare the diff against the spec the task names, then
53
61
  \`mcp__relai__submit_review\` \u2014 \`approve\`, or \`reject\` with a specific note the author
54
- can act on. Do this even when get_my_tasks is empty.
62
+ can act on. When the task is a goal's build with a PR, pass \`headSha\`: the full sha of the
63
+ commit you reviewed. Do this even when get_my_tasks is empty.
55
64
  If the change touches a long-running service or a third-party runtime integration (SSE/event streams, launchd/systemd, auth headers, a client library), run the changed path once before approving and quote what it printed. A type cast past a library's own types is a reason to stop and run it.
56
65
 
57
66
  4. Call \`mcp__relai__get_my_tasks\` \u2014 if it returns no tasks, stop immediately.
@@ -293,7 +302,11 @@ Find the default branch with \`git remote show origin\`.
293
302
  \`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
294
303
  the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
295
304
  \`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
296
- Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
305
+ Before writing any build task, choose the goal's reviewer (for a \`reviewer_agent\` gate): a
306
+ \`reviewer\` agent, on a different \`workerType\` from the writers when one exists. Never assign that
307
+ agent a build task, and never leave the only reviewers busy building: a goal whose every reviewer
308
+ built part of it has nobody left to review it. The API defaults a build task with no
309
+ \`specialization\` to \`writer\` when the repo has writers. Make them small
297
310
  and independently testable so several agents can work at once. Use \`blockedBy\` only where
298
311
  one task truly needs another's result. Each description is self-contained: what to change,
299
312
  which files, how to test it, and the acceptance criteria.
@@ -313,6 +326,62 @@ Find the default branch with \`git remote show origin\`.
313
326
  Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
314
327
  the planning task's thread and complete it.
315
328
 
329
+ **Review goals** (the order's \`kind\` is \`"review"\`; the planning task carries
330
+ \`metadata.orderKind: "review"\`): the goal is one review posted on the existing pull request at the
331
+ order's \`prUrl\`. There is no order branch, no draft PR and no merge, so skip steps 2 to 6 above.
332
+ Nobody pushes to the PR's branch, approves it, or requests changes on it.
333
+ 1. As the planner, read the PR with \`gh pr view <prUrl>\` and \`gh pr diff <prUrl>\`. Pick the
334
+ angles it needs: correctness, security, simplicity, tests and consistency with the codebase for a
335
+ real change; correctness and consistency alone for a small, low-risk one (docs, tests, under about
336
+ 150 changed lines with no auth, input handling, secrets, money or migrations).
337
+ 2. One task per angle, \`epicId\` = the order's \`epicId\`,
338
+ \`metadata: { orderId, orderRole: "review", angle: "<angle>", prUrl }\`. Spread the angles
339
+ across vendors: assign each to a different agent from \`mcp__relai__list_agents\`, preferring a
340
+ different \`workerType\` from the agents already given one, and reuse agents only when there are
341
+ more angles than agents. Each description names the angle, the PR, what that angle hunts for, and
342
+ the reporting format below.
343
+ 3. One lead task, \`blockedBy\` every angle task, assigned to yourself, with
344
+ \`metadata: { orderId, orderRole: "review_lead", prUrl }\`.
345
+ 4. When acceptance includes \`human_approval\`, add a gate task blocked by the lead
346
+ (\`metadata: { orderId, orderRole: "gate" }\`, \`verifyKind: "human_approval"\`) and a post task
347
+ blocked by the gate (\`metadata: { orderId, orderRole: "post_review", prUrl }\`). Without it,
348
+ the lead posts the review itself.
349
+ 5. If the order has \`relatedRepos\`, copy them into every angle and lead task's metadata and name them in
350
+ each description, so reviewers check the contracts this PR depends on in those repos.
351
+ 6. Post the task list on the planning task's thread and complete it.
352
+
353
+ **orderRole "review"** (one angle of a review goal):
354
+ - Read the diff and as much surrounding code as you need. To run anything, check out the PR head
355
+ detached: \`git fetch origin pull/<n>/head && git checkout --detach FETCH_HEAD\`. Never push, never
356
+ comment on the PR, never edit the repo's history.
357
+ - Verify by running code where you can. Report your 3 to 5 best-evidenced findings for your angle, most
358
+ severe first, each with severity (blocker, high, medium, low), file:line, and a concrete failing
359
+ input or scenario. "No findings" is a valid report. A missing code comment is not a finding.
360
+ - When the task names \`relatedRepos\`, read them for any contract this change depends on (an endpoint it
361
+ calls, a helper it imports, a config value it reads). Read only, outside your worktree: a shallow
362
+ clone into a temp directory (\`gh repo clone <url> <tmp dir> -- --depth 1\`) or one file at a time with
363
+ \`gh api repos/<owner>/<repo>/contents/<path>\`. Never push, branch, comment or open anything there.
364
+ A finding that rests on a related repo cites the file and line there. If you could not read the
365
+ repo, mark that finding UNPROVEN and say why rather than guessing what the other side does.
366
+ - Post the report with \`mcp__relai__add_task_comment\` on this task, then complete it.
367
+
368
+ **orderRole "review_lead"**:
369
+ - Read every angle task's report with \`mcp__relai__get_task_comments\`. Merge duplicates, drop
370
+ findings you cannot reproduce or whose premise is wrong, and rank what is left. Keep an UNPROVEN mark
371
+ on any finding about a related repo nobody could read. Severity sets the
372
+ verdict: any blocker or high means changes are needed; otherwise it is safe to merge with the
373
+ noted follow-ups.
374
+ - Write one review: the verdict in the first line, then the findings, each with file:line, the
375
+ failure and the fix you would ask for, then which angles ran, each with its agent's name and vendor
376
+ (\`workerType\` from \`mcp__relai__list_agents\`), and which related repos were read. Keep it short.
377
+ - Without a \`human_approval\` gate, post it with \`gh pr review <prUrl> --comment --body-file <file>\`
378
+ and put the review URL on the task's thread. With a gate, post the review as a comment on this task
379
+ for the owner to approve, and do not touch the PR. Then complete.
380
+
381
+ **orderRole "post_review"**: once the gate is approved, post the lead's review comment from the
382
+ lead task's thread with \`gh pr review <prUrl> --comment --body-file <file>\`, put the review URL on
383
+ this task's thread, and complete.
384
+
316
385
  **orderRole "plan_review"** (assigned to the planner, reviewed by another agent):
317
386
  - After the task graph is written, complete each plan review task. That hands the plan to its
318
387
  reviewer. Every build task waits until all plan reviews are approved.
@@ -498,6 +567,18 @@ async function recordSessionUsage(conn, startIds, usage, logPrefix) {
498
567
  await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
499
568
  }
500
569
 
570
+ // ../claude-worker/src/model.ts
571
+ import { readFileSync } from "node:fs";
572
+ function pinnedModel(env = process.env) {
573
+ const file = env.PITBOSS_MODEL_FILE;
574
+ if (!file) return void 0;
575
+ try {
576
+ return readFileSync(file, "utf-8").trim() || void 0;
577
+ } catch {
578
+ return void 0;
579
+ }
580
+ }
581
+
501
582
  // ../claude-worker/src/session.ts
502
583
  function mcpServerEntry() {
503
584
  return fileURLToPath(new URL("../../mcp-server/src/index.ts", import.meta.url));
@@ -546,7 +627,7 @@ async function runClaudeSession(config2) {
546
627
  "stream-json",
547
628
  "--verbose",
548
629
  "--model",
549
- config2.model,
630
+ pinnedModel() ?? config2.model,
550
631
  "--add-dir",
551
632
  config2.repoPath
552
633
  ];
@@ -667,8 +748,8 @@ function classifySessionError(text) {
667
748
 
668
749
  // ../claude-worker/src/adapters/cursor.ts
669
750
  var bin = () => process.env.CURSOR_BIN ?? "cursor-agent";
670
- function cursorArgs(repoPath, prompt) {
671
- return ["-p", "--trust", "--force", "--output-format", "stream-json", "--workspace", repoPath, prompt];
751
+ function cursorArgs(repoPath, prompt, model) {
752
+ return ["-p", "--trust", "--force", "--output-format", "stream-json", "--workspace", repoPath, ...model ? ["--model", model] : [], prompt];
672
753
  }
673
754
  function readCursorEvent(line) {
674
755
  let event;
@@ -692,7 +773,7 @@ function readCursorEvent(line) {
692
773
  return {};
693
774
  }
694
775
  async function runCursorSession(config2) {
695
- const args = cursorArgs(config2.repoPath, buildPrompt(config2));
776
+ const args = cursorArgs(config2.repoPath, buildPrompt(config2), pinnedModel());
696
777
  console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
697
778
  const taskIdsPromise = listSessionTaskIds(config2, "[cursor-worker]");
698
779
  return new Promise((resolve, reject) => {
@@ -773,10 +854,11 @@ import { unlinkSync as unlinkSync2 } from "fs";
773
854
  var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
774
855
  var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
775
856
  var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
776
- function copilotArgs(repoPath, prompt, mcpConfigPath) {
857
+ function copilotArgs(repoPath, prompt, mcpConfigPath, model) {
777
858
  return [
778
859
  "-p",
779
860
  prompt,
861
+ ...model ? ["--model", model] : [],
780
862
  "--allow-all-tools",
781
863
  "--output-format",
782
864
  "json",
@@ -806,7 +888,7 @@ function readCopilotEvent(line) {
806
888
  }
807
889
  async function runCopilotSession(config2) {
808
890
  const mcpConfigPath = writeMcpConfig(config2, "copilot-worker");
809
- const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath);
891
+ const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath, pinnedModel());
810
892
  console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
811
893
  return new Promise((resolve, reject) => {
812
894
  const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -879,7 +961,7 @@ var bin3 = () => process.env.CODEX_BIN ?? "codex";
879
961
  var codexHome = () => process.env.CODEX_HOME ?? join3(homedir(), ".codex");
880
962
  var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
881
963
  var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
882
- function codexArgs(repoPath, prompt, profile) {
964
+ function codexArgs(repoPath, prompt, profile, model) {
883
965
  return [
884
966
  "exec",
885
967
  "--json",
@@ -890,6 +972,7 @@ function codexArgs(repoPath, prompt, profile) {
890
972
  repoPath,
891
973
  "-p",
892
974
  profile,
975
+ ...model ? ["-m", model] : [],
893
976
  prompt
894
977
  ];
895
978
  }
@@ -938,7 +1021,7 @@ async function runCodexSession(config2) {
938
1021
  } catch {
939
1022
  }
940
1023
  };
941
- const args = codexArgs(config2.repoPath, buildPrompt(config2), profile);
1024
+ const args = codexArgs(config2.repoPath, buildPrompt(config2), profile, pinnedModel());
942
1025
  console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
943
1026
  return new Promise((resolve, reject) => {
944
1027
  const proc = spawn4(bin3(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -1223,6 +1306,10 @@ function loadConfig2() {
1223
1306
  };
1224
1307
  }
1225
1308
 
1309
+ // ../event-worker/src/worker.ts
1310
+ import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
1311
+ import { dirname as dirname2 } from "node:path";
1312
+
1226
1313
  // ../../node_modules/.pnpm/eventsource-parser@3.0.7/node_modules/eventsource-parser/dist/index.js
1227
1314
  var ParseError = class extends Error {
1228
1315
  constructor(message, options) {
@@ -1783,7 +1870,16 @@ async function runEventWorker(config2) {
1783
1870
  }
1784
1871
  try {
1785
1872
  console.log("[event-worker] Event received \u2014 running session...");
1786
- await getAdapter(config2.runtime).runSession(config2);
1873
+ const activityFile = process.env.PITBOSS_ACTIVITY_FILE;
1874
+ if (activityFile) {
1875
+ mkdirSync2(dirname2(activityFile), { recursive: true });
1876
+ writeFileSync3(activityFile, String(Date.now()));
1877
+ }
1878
+ try {
1879
+ await getAdapter(config2.runtime).runSession(config2);
1880
+ } finally {
1881
+ if (activityFile) rmSync(activityFile, { force: true });
1882
+ }
1787
1883
  await clearUnavailable(config2, "[event-worker]");
1788
1884
  } catch (err) {
1789
1885
  const text = err instanceof Error ? err.message : String(err);
package/dist/mcp.js CHANGED
@@ -12270,8 +12270,10 @@ var ApiClient = class {
12270
12270
  listRepos() {
12271
12271
  return this.request("GET", "/repos");
12272
12272
  }
12273
- createRepo(body) {
12274
- return this.request("POST", "/repos", body);
12273
+ async createRepo(body) {
12274
+ const json = await this.requestEnvelope("POST", "/repos", body);
12275
+ const next = json.next;
12276
+ return { repo: json.data, ...next ? { next } : {} };
12275
12277
  }
12276
12278
  // Used at startup to decide whether this credential carries owner scope. The
12277
12279
  // `current` flag marks the row that authenticated the request, which is the
@@ -12353,6 +12355,9 @@ var ApiClient = class {
12353
12355
  submitReview(taskId, body) {
12354
12356
  return this.request("POST", `/tasks/${taskId}/review`, body);
12355
12357
  }
12358
+ approveGate(taskId, note) {
12359
+ return this.request("POST", `/tasks/${taskId}/approve`, { decision: "approve", ...note ? { note } : {} });
12360
+ }
12356
12361
  startReview(taskId) {
12357
12362
  return this.request("POST", `/tasks/${taskId}/review/start`);
12358
12363
  }
@@ -12436,6 +12441,9 @@ var ApiClient = class {
12436
12441
  listPools(repoId) {
12437
12442
  return this.request("GET", `/repos/${encodeURIComponent(repoId)}/pools`);
12438
12443
  }
12444
+ updateRepo(repoId, body) {
12445
+ return this.request("PUT", `/repos/${repoId}`, body);
12446
+ }
12439
12447
  setPoolSize(repoId, name, body) {
12440
12448
  return this.request("PUT", `/repos/${encodeURIComponent(repoId)}/pools/${encodeURIComponent(name)}`, body);
12441
12449
  }
@@ -12538,19 +12546,23 @@ function buildCreateOrderTool(client, repoId, opts) {
12538
12546
  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."),
12539
12547
  feature: external_exports.string().min(1).optional().describe("What to build, in the requester's words. Optional when issueUrl is given."),
12540
12548
  issueUrl: external_exports.string().optional().describe("A GitHub issue in this repo to build from (https://github.com/<owner>/<repo>/issues/<n>). The planner reads it, and the merge closes it."),
12549
+ reviewPrUrl: external_exports.string().optional().describe('Review this pull request in the repo instead of building something (https://github.com/<owner>/<repo>/pull/<n>). Agents on different vendors each review one angle, a lead merges their findings, and one review is posted on the PR. No gate by default; pass acceptance ["human_approval"] to approve the review before it is posted. feature, when given, narrows what to look at.'),
12550
+ relatedRepos: external_exports.array(external_exports.string()).max(5).optional().describe("Review goals only: ids of other repos you own (see list_repos) whose contracts this PR depends on. Reviewers read them, never write to them, and mark findings about one they could not read as unproven."),
12541
12551
  planReviewers: external_exports.number().int().min(0).max(3).optional().describe("How many other agents must approve the plan before building starts (0 to 3). Reviewers are picked by the API, preferring reviewer agents on another vendor."),
12542
12552
  pairBuilds: external_exports.boolean().optional().describe("Pair every build task with a reviewer on a different vendor: each build carries its own review gate, and the builder is never on the reviewer's vendor."),
12543
12553
  title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal. The planner writes one when omitted."),
12544
- acceptance: external_exports.array(external_exports.enum(["ci_passed", "human_approval", "reviewer_agent"])).optional().describe("Gates that must all pass. Defaults to human_approval."),
12554
+ acceptance: external_exports.array(external_exports.enum(["ci_passed", "human_approval", "reviewer_agent"])).optional().describe("Gates that must all pass. Defaults to human_approval, or none for a review."),
12545
12555
  budget: external_exports.record(external_exports.unknown()).optional().describe("Optional ceilings. Stored only; not enforced.")
12546
12556
  }),
12547
12557
  handler: async (input) => {
12548
- if (!input.feature && !input.issueUrl) return mcpFail("create_order needs feature or issueUrl");
12558
+ if (!input.feature && !input.issueUrl && !input.reviewPrUrl) return mcpFail("create_order needs feature, issueUrl or reviewPrUrl");
12549
12559
  try {
12550
12560
  const created = await client.createOrder({
12551
12561
  repoId: targetRepo(input),
12552
12562
  ...input.feature ? { feature: input.feature } : {},
12553
12563
  ...input.issueUrl ? { issueUrl: input.issueUrl } : {},
12564
+ ...input.reviewPrUrl ? { kind: "review", prUrl: input.reviewPrUrl } : {},
12565
+ ...input.relatedRepos?.length ? { relatedRepos: input.relatedRepos } : {},
12554
12566
  ...input.planReviewers ? { planReviewers: input.planReviewers } : {},
12555
12567
  ...input.pairBuilds ? { pairBuilds: true } : {},
12556
12568
  ...input.title ? { title: input.title } : {},
@@ -12596,7 +12608,7 @@ function buildListPoolsTool(client, repoId, opts) {
12596
12608
  const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
12597
12609
  return {
12598
12610
  name: "list_pools",
12599
- description: "List the worker pool sizes set for a repo. " + POOL_NOTE,
12611
+ description: "List the worker pool sizes set for a repo. Each pool carries `members`: its agents (named <pool>-<n>) with `activity` (working, reviewing, waiting or idle) and `activitySince`, reported by pitboss node from outside the runtime; both are null until a node reports. " + POOL_NOTE,
12600
12612
  inputSchema: external_exports.object({
12601
12613
  repoId: external_exports.string().optional().describe("Repo to read. Defaults to this agent's home repo.")
12602
12614
  }),
@@ -12631,6 +12643,31 @@ function buildSetPoolSizeTool(client, repoId, opts) {
12631
12643
  }
12632
12644
  };
12633
12645
  }
12646
+ function buildSetRepoOperationsTool(client, repoId, opts) {
12647
+ const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
12648
+ return {
12649
+ name: "set_repo_operations",
12650
+ description: "Record how a repo is operated so every agent sees it on session_start: the GitHub account its agents work as, the node config dir and spec path that run it, and the test-database rule. Names and paths only, never a token or secret. Replaces the whole record; pass every field you want kept." + (opts.ownerScoped ? " As the owner's agent you may pass repoId for any repo your owner owns." : ""),
12651
+ inputSchema: external_exports.object({
12652
+ repoId: external_exports.string().optional().describe(opts.ownerScoped ? "Repo to update. Defaults to your home repo." : "Ignored; this agent's repo is updated."),
12653
+ githubAccount: external_exports.string().optional().describe("GitHub login agents push and run gh as, e.g. 'phillipsio'."),
12654
+ nodeConfigDir: external_exports.string().optional().describe("PITBOSS_CONFIG_DIR of the node that runs this repo."),
12655
+ nodeSpecPath: external_exports.string().optional().describe("Path of that node's spec.json."),
12656
+ testDatabase: external_exports.string().optional().describe("The rule for test databases, in one line.")
12657
+ }),
12658
+ handler: async (input) => {
12659
+ const { repoId: _repo, ...fields } = input;
12660
+ const operations = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0));
12661
+ try {
12662
+ const target = targetRepo(input);
12663
+ await client.updateRepo(target, { operations });
12664
+ return { content: [{ type: "text", text: JSON.stringify({ repoId: target, operations }, null, 2) }] };
12665
+ } catch (err) {
12666
+ return mcpFail(err);
12667
+ }
12668
+ }
12669
+ };
12670
+ }
12634
12671
  function resolveAgentRef(candidates, ref, scope) {
12635
12672
  const available = candidates.length ? ` Available: ${candidates.map((a) => `${a.name} (${a.id})`).join(", ")}` : ` ${scope} has no agents.`;
12636
12673
  if (ref.startsWith("agent_")) {
@@ -12709,7 +12746,7 @@ ${version.body}`
12709
12746
  },
12710
12747
  {
12711
12748
  name: "list_agents",
12712
- description: "List the agents you can see, so you can address one. Covers every repo sharing this one's owner, not just your own, so a peer working on a sibling repo can be found. Archived agents (and agents whose repo is archived) are omitted. Returns each agent's id, name, role, specialization, domains and repoId, plus `online` (seen within the last 10 minutes \u2014 the same window the routing scheduler uses), `listening` (a GET /events write succeeded in this API process within about two heartbeat windows), and `isYou` on your own entry. Use it before send_message when you need to ask a specific peer something: pick by specialization or domain for who is likely to know, and prefer an online agent for a timely reply. Seeing an agent does not mean you can read its work \u2014 tasks, non-DM threads and repo events stay scoped to their own repo. Direct messages reach any agent you can see (a repo sharing this owner), and a DM thread is readable only by its two participants." + (opts.ownerScoped ? " As the owner's agent you are the exception: you can coordinate tasks and threads in any repo your owner owns." : ""),
12749
+ description: "List the agents you can see, so you can address one. Covers every repo sharing this one's owner, not just your own, so a peer working on a sibling repo can be found. Archived agents (and agents whose repo is archived) are omitted. Returns each agent's id, name, role, specialization, domains and repoId, plus `online` (seen within the last 10 minutes \u2014 the same window the routing scheduler uses), `listening` (a GET /events write succeeded in this API process within about two heartbeat windows), and `isYou` on your own entry, plus `activity` (working, reviewing, waiting or idle) with `activitySince`, reported by pitboss node from outside the runtime (null until a node reports), and `idleWithReadyMs24h`. Use it before send_message when you need to ask a specific peer something: pick by specialization or domain for who is likely to know, and prefer an online agent for a timely reply. Seeing an agent does not mean you can read its work \u2014 tasks, non-DM threads and repo events stay scoped to their own repo. Direct messages reach any agent you can see (a repo sharing this owner), and a DM thread is readable only by its two participants." + (opts.ownerScoped ? " As the owner's agent you are the exception: you can coordinate tasks and threads in any repo your owner owns." : ""),
12713
12750
  inputSchema: external_exports.object({}),
12714
12751
  handler: async () => {
12715
12752
  const raw = await client.listAgents();
@@ -12725,6 +12762,9 @@ ${version.body}`
12725
12762
  lastSeenAt: a.lastSeenAt,
12726
12763
  online: typeof a.lastSeenAt === "string" ? now - new Date(a.lastSeenAt).getTime() < ONLINE_WINDOW_MS : false,
12727
12764
  listening: a.listening === true,
12765
+ activity: a.activity ?? null,
12766
+ activitySince: a.activitySince ?? null,
12767
+ idleWithReadyMs24h: typeof a.idleWithReadyMs24h === "number" ? a.idleWithReadyMs24h : 0,
12728
12768
  ...a.id === agentId ? { isYou: true } : {}
12729
12769
  }));
12730
12770
  return {
@@ -12909,10 +12949,10 @@ ${version.body}`
12909
12949
  },
12910
12950
  {
12911
12951
  name: "check_authority",
12912
- description: "Ask the API whether a server-stamped authority id is currently valid for a class (decide or assign). metadata.authority on a message or task is a stamp id, not permission. Reads may also include an authority object with valid/reason; that is informational and can go stale. The API enforces at the action; use this when you need to re-check before acting.",
12952
+ description: "Ask the API whether a server-stamped authority id is currently valid for a class (decide, assign or approve). metadata.authority on a message or task is a stamp id, not permission. Reads may also include an authority object with valid/reason; that is informational and can go stale. The API enforces at the action; use this when you need to re-check before acting.",
12913
12953
  inputSchema: external_exports.object({
12914
12954
  id: external_exports.string().describe("The stamp id from metadata.authority or an authority.id field."),
12915
- class: external_exports.enum(["decide", "assign"]).describe("The action class you intend to rely on.")
12955
+ class: external_exports.enum(["decide", "assign", "approve"]).describe("The action class you intend to rely on.")
12916
12956
  }),
12917
12957
  handler: async (input) => {
12918
12958
  const authority = await client.checkAuthority(input.id, input.class);
@@ -13049,6 +13089,7 @@ ${version.body}`
13049
13089
  payload.reminder = "unreadMessages above may need a reply \u2014 call send_message with the same threadId. The sender will not see anything you only write in your own session.";
13050
13090
  payload.peerBoundary = PEER_BOUNDARY;
13051
13091
  }
13092
+ if (session.repo?.operations) payload.peerBoundary = PEER_BOUNDARY;
13052
13093
  const orientation = session.orientation;
13053
13094
  const omitted = [
13054
13095
  describeCap("unread messages", session.unreadCount, unread.length, "get_unread_messages"),
@@ -13144,10 +13185,17 @@ ${version.body}`
13144
13185
  inputSchema: external_exports.object({
13145
13186
  taskId: external_exports.string().describe("The ID of the task you are reviewing."),
13146
13187
  decision: external_exports.enum(["approve", "reject"]).describe("Approve or reject."),
13147
- note: external_exports.string().max(2e3).optional().describe("Required for reject; useful for approve. Concise reason or guidance.")
13188
+ note: external_exports.string().max(2e3).optional().describe("Required for reject; useful for approve. Concise reason or guidance."),
13189
+ headSha: external_exports.string().regex(/^[0-9a-f]{40}$/i).optional().describe(
13190
+ "The full commit sha you reviewed (the PR head, from `git rev-parse HEAD` on its branch). An approval of a goal's build counts toward the owner's delegated gate only for this exact commit."
13191
+ )
13148
13192
  }),
13149
13193
  handler: async (input) => {
13150
- const task = await client.submitReview(input.taskId, { decision: input.decision, note: input.note });
13194
+ const task = await client.submitReview(input.taskId, {
13195
+ decision: input.decision,
13196
+ note: input.note,
13197
+ ...input.headSha ? { headSha: input.headSha } : {}
13198
+ });
13151
13199
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
13152
13200
  }
13153
13201
  },
@@ -13227,7 +13275,7 @@ ${version.body}`
13227
13275
  ...opts.ownerScoped || opts.role === "orchestrator" ? [buildCreateOrderTool(client, repoId, opts)] : [],
13228
13276
  ...opts.role === "orchestrator" || (opts.tier ?? 0) >= 2 ? [buildUpdateOrderTool(client)] : [],
13229
13277
  buildListPoolsTool(client, repoId, opts),
13230
- ...opts.ownerScoped || opts.role === "orchestrator" ? [buildSetPoolSizeTool(client, repoId, opts)] : [],
13278
+ ...opts.ownerScoped || opts.role === "orchestrator" ? [buildSetPoolSizeTool(client, repoId, opts), buildSetRepoOperationsTool(client, repoId, opts)] : [],
13231
13279
  // Additive, never a swap: an owner-scoped agent is still an agent and needs
13232
13280
  // everything above. buildOperatorTools assumes no agent identity, and its
13233
13281
  // create_task bypasses propose/commit.
@@ -13256,11 +13304,29 @@ async function staleAttentionCount(client, cutoff) {
13256
13304
  const { meta } = await client.getTasksPage({ status: ATTENTION_STATUSES, updatedBefore: cutoff, limit: 1, clip: true });
13257
13305
  return meta?.total ?? 0;
13258
13306
  }
13307
+ var SILENT_LABEL = /^Reviewing, silent (\d+)m$/;
13308
+ async function reviewSilentItems(client, tasks) {
13309
+ const silent = tasks.flatMap((t) => {
13310
+ const m = typeof t.humanLabel === "string" ? SILENT_LABEL.exec(t.humanLabel) : null;
13311
+ return m ? [{ task: t, silentMinutes: Number(m[1]) }] : [];
13312
+ });
13313
+ if (silent.length === 0) return [];
13314
+ const agents = await client.listAgents();
13315
+ const names = new Map(agents.map((a) => [a.id, a.name]));
13316
+ return silent.map(({ task, silentMinutes }) => ({
13317
+ taskId: task.id,
13318
+ title: task.title,
13319
+ repoId: task.repoId,
13320
+ reviewerId: task.verifyReviewerId ?? null,
13321
+ reviewer: names.get(task.verifyReviewerId) ?? null,
13322
+ silentMinutes
13323
+ }));
13324
+ }
13259
13325
  function buildGodCoordinationTools(client) {
13260
13326
  return [
13261
13327
  {
13262
13328
  name: "list_attention",
13263
- description: "List what needs attention across ALL your owner's repos: tasks that are 'blocked' (a worker is waiting), 'pending_verification' (awaiting a review decision), or 'proposed' (a worker's task awaiting commit), plus idle workers that cannot take the ready work that is sitting there (idle_unroutable \u2014 derived now, not from the event log). Each task carries its repoId. A blocked task with metadata.blockedThreadId resumes when you answer with send_message (type decision) on that thread, as if the owner had, when the owner has given you a decide delegation for that repo; the task records clearedBy. Answer only reversible, branch-level confirmations yourself (editing, pushing or deleting files on an order branch, choosing between plan options). Merges into a default branch, deploys, deletes outside an order branch, and anything touching credentials or money go to the owner. One without it (stalled or overflowed work, or your own task) needs reassigning with assign_task or splitting instead. An idle_unroutable item names the agent and why each task is ineligible (domain, specialization, review_gate, for-human, product-decision, pinned, blockedBy). Capped and clipped, newest first; read one in full with get_task.",
13329
+ description: "List what needs attention across ALL your owner's repos: tasks that are 'blocked' (a worker is waiting), 'pending_verification' (awaiting a review decision), or 'proposed' (a worker's task awaiting commit), plus idle workers that cannot take the ready work that is sitting there (idle_unroutable \u2014 derived now, not from the event log), and reviewSilent: reviews whose reviewer sent no heartbeat for 40 minutes, naming the reviewer and silent minutes. Each task carries its repoId. A blocked task with metadata.blockedThreadId resumes when you answer with send_message (type decision) on that thread, as if the owner had, when the owner has given you a decide delegation for that repo; the task records clearedBy. Answer only reversible, branch-level confirmations yourself (editing, pushing or deleting files on an order branch, choosing between plan options). Merges into a default branch, deploys, deletes outside an order branch, and anything touching credentials or money go to the owner. A blocked task without blockedThreadId (stalled or overflowed work) resumes the same way when you post add_task_comment (type decision) on it; say whether to split, retry or reassign. Your own blocked task never resumes on your answer. An idle_unroutable item names the agent and why each task is ineligible (domain, specialization, review_gate, for-human, product-decision, pinned, blockedBy). Capped and clipped, newest first; read one in full with get_task.",
13264
13330
  inputSchema: external_exports.object(attentionWindowSchema),
13265
13331
  handler: async (input) => {
13266
13332
  const cutoff = attentionCutoff(input);
@@ -13278,6 +13344,7 @@ function buildGodCoordinationTools(client) {
13278
13344
  const idle = idleUnroutable ?? [];
13279
13345
  const total = meta?.total ?? tasks.length;
13280
13346
  const notShown = describeCap("tasks", total, tasks.length, "get_task on a specific id");
13347
+ const reviewSilent = await reviewSilentItems(client, tasks);
13281
13348
  return {
13282
13349
  content: [{
13283
13350
  type: "text",
@@ -13285,6 +13352,7 @@ function buildGodCoordinationTools(client) {
13285
13352
  tasks,
13286
13353
  taskCount: total,
13287
13354
  ...idle.length > 0 ? { idleUnroutable: idle } : {},
13355
+ ...reviewSilent.length > 0 ? { reviewSilent } : {},
13288
13356
  ...notShown ? { notShown } : {},
13289
13357
  ...staleCount > 0 ? { staleCount, stale: `${staleCount} older items hidden; pass includeStale: true to see them, and cancel or archive what is dead.` } : {},
13290
13358
  peerBoundary: PEER_BOUNDARY
@@ -13292,6 +13360,22 @@ function buildGodCoordinationTools(client) {
13292
13360
  }]
13293
13361
  };
13294
13362
  }
13363
+ },
13364
+ {
13365
+ name: "approve_gate",
13366
+ description: "Approve a goal's human_approval gate for the owner. The API allows it only when the owner gave you an approve delegation for that repo and every reviewer gate in the goal passed. Under a github-evidence grant it also needs CI green on the PR's head, an open PR with no migration, and a size within the risk tier; under a pitboss-evidence grant the reviews are enough and the goal's merge step checks CI. Otherwise it refuses and lists every unmet condition; tell the owner rather than retrying. You can only approve, never reject. The task records clearedBy with the evidence. Find gates with list_attention (pending_verification, verifyKind human_approval).",
13367
+ inputSchema: external_exports.object({
13368
+ taskId: external_exports.string().min(1).describe("The gate task id."),
13369
+ note: external_exports.string().max(2e3).optional().describe("Why you approved, in one line.")
13370
+ }),
13371
+ handler: async (input) => {
13372
+ try {
13373
+ const task = await client.approveGate(input.taskId, input.note);
13374
+ return { content: [{ type: "text", text: JSON.stringify({ task, peerBoundary: PEER_BOUNDARY }, null, 2) }] };
13375
+ } catch (err) {
13376
+ return mcpFail(err);
13377
+ }
13378
+ }
13295
13379
  }
13296
13380
  ];
13297
13381
  }
@@ -13371,12 +13455,12 @@ function buildProvisioningTools(client, opts = {}) {
13371
13455
  )
13372
13456
  }),
13373
13457
  handler: async (input) => {
13374
- const repo = await client.createRepo({
13458
+ const created = await client.createRepo({
13375
13459
  name: input.name,
13376
13460
  ...input.description ? { description: input.description } : {},
13377
13461
  ...input.repoUrl ? { repoUrl: input.repoUrl } : {}
13378
13462
  });
13379
- return { content: [{ type: "text", text: JSON.stringify({ repo }, null, 2) }] };
13463
+ return { content: [{ type: "text", text: JSON.stringify(created, null, 2) }] };
13380
13464
  }
13381
13465
  },
13382
13466
  // Mints a bearer credential: owner mode and the god credential only.
@@ -13451,7 +13535,7 @@ function buildOperatorTools(client, ownerId) {
13451
13535
  ...buildProvisioningTools(client, { credentialMinting: true }),
13452
13536
  {
13453
13537
  name: "list_agents",
13454
- description: "List agents across your repos, with a computed online field (true if seen within the last 10 minutes \u2014 the same window the routing scheduler uses) and `listening` (a GET /events write succeeded in this API process within about two heartbeat windows). Archived agents and agents in archived repos are omitted. Pass repoId to scope to one repo; omit to list across all your repos. Use this to see who is available before creating a task or posting a message.",
13538
+ description: "List agents across your repos, with a computed online field (true if seen within the last 10 minutes \u2014 the same window the routing scheduler uses) and `listening` (a GET /events write succeeded in this API process within about two heartbeat windows), plus `activity` (working, reviewing, waiting or idle) with `activitySince`, reported by pitboss node from outside the runtime (null until a node reports), and `idleWithReadyMs24h`. Archived agents and agents in archived repos are omitted. Pass repoId to scope to one repo; omit to list across all your repos. Use this to see who is available before creating a task or posting a message.",
13455
13539
  inputSchema: external_exports.object({
13456
13540
  repoId: external_exports.string().optional().describe("Scope to a specific repo. Omit to list across all your repos.")
13457
13541
  }),
@@ -13467,7 +13551,10 @@ function buildOperatorTools(client, ownerId) {
13467
13551
  repoId: a.repoId,
13468
13552
  lastSeenAt: a.lastSeenAt,
13469
13553
  online: typeof a.lastSeenAt === "string" ? now - new Date(a.lastSeenAt).getTime() < ONLINE_WINDOW_MS : false,
13470
- listening: a.listening === true
13554
+ listening: a.listening === true,
13555
+ activity: a.activity ?? null,
13556
+ activitySince: a.activitySince ?? null,
13557
+ idleWithReadyMs24h: typeof a.idleWithReadyMs24h === "number" ? a.idleWithReadyMs24h : 0
13471
13558
  }));
13472
13559
  return {
13473
13560
  content: [{
@@ -13557,7 +13644,7 @@ function buildOperatorTools(client, ownerId) {
13557
13644
  },
13558
13645
  {
13559
13646
  name: "list_attention",
13560
- description: "List everything across ALL your projects that needs you right now: tasks that are 'blocked' (a worker is waiting on your input), 'pending_verification' (awaiting a review decision), or 'proposed' (a worker's task awaiting your commit), plus idle workers that cannot take the ready work sitting there (idle_unroutable). Call this first to see what to act on. Each task carries its repoId and, for blocked tasks, metadata.blockedThreadId \u2014 the thread to post a reply on (via reply_human) to resume the worker. An idle_unroutable item names the agent and why each task is ineligible.",
13647
+ description: "List everything across ALL your projects that needs you right now: tasks that are 'blocked' (a worker is waiting on your input), 'pending_verification' (awaiting a review decision), or 'proposed' (a worker's task awaiting your commit), plus idle workers that cannot take the ready work sitting there (idle_unroutable), and reviewSilent: reviews whose reviewer has sent no heartbeat for 40 minutes, with the reviewer and silent minutes. Call this first to see what to act on. Each task carries its repoId and, for blocked tasks, metadata.blockedThreadId \u2014 the thread to post a reply on (via reply_human) to resume the worker. An idle_unroutable item names the agent and why each task is ineligible.",
13561
13648
  inputSchema: external_exports.object(attentionWindowSchema),
13562
13649
  handler: async (input) => {
13563
13650
  const cutoff = attentionCutoff(input);
@@ -13567,12 +13654,14 @@ function buildOperatorTools(client, ownerId) {
13567
13654
  staleAttentionCount(client, cutoff)
13568
13655
  ]);
13569
13656
  const idle = idleUnroutable ?? [];
13657
+ const reviewSilent = await reviewSilentItems(client, tasks);
13570
13658
  return {
13571
13659
  content: [{
13572
13660
  type: "text",
13573
13661
  text: tasks.length === 0 && idle.length === 0 && staleCount === 0 ? "Nothing needs your attention across your projects right now." : JSON.stringify({
13574
13662
  tasks,
13575
13663
  ...idle.length > 0 ? { idleUnroutable: idle } : {},
13664
+ ...reviewSilent.length > 0 ? { reviewSilent } : {},
13576
13665
  ...staleCount > 0 ? { staleCount, stale: `${staleCount} older items hidden; pass includeStale: true to see them, and cancel or archive what is dead.` } : {}
13577
13666
  }, null, 2)
13578
13667
  }]
@@ -13845,7 +13934,8 @@ function attentionStateOf(task) {
13845
13934
  function describe(task, state) {
13846
13935
  const title = (task.title ?? task.id).slice(0, 80);
13847
13936
  const where = task.repoId ? ` [${task.repoId}]` : "";
13848
- const thread = state === "blocked" && typeof task.metadata?.blockedThreadId === "string" ? ` Reply on thread ${task.metadata.blockedThreadId} to unblock it.` : "";
13937
+ const replyOn = typeof task.metadata?.blockedThreadId === "string" ? task.metadata.blockedThreadId : task.threadId;
13938
+ const thread = state !== "blocked" ? "" : replyOn ? ` Reply on thread ${replyOn} to unblock it.` : ` Answer with add_task_comment on ${task.id} to unblock it.`;
13849
13939
  const next = state === "proposed" ? " Use commit_proposal." : state === "pending_verification" ? " Use review_task." : state === "stalled" ? " Nobody is coming; reassign or cancel it." : "";
13850
13940
  return `relai: "${title}" is ${LABEL[state]}${where} (${task.id}).${thread}${next}`;
13851
13941
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitboss/cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [