@pitboss/cli 0.4.8 → 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.
@@ -59,7 +59,8 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
59
59
  a one-line status on the task thread naming what you are checking) when you begin, then
60
60
  run the tests, compare the diff against the spec the task names, then
61
61
  \`mcp__relai__submit_review\` \u2014 \`approve\`, or \`reject\` with a specific note the author
62
- 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.
63
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.
64
65
 
65
66
  4. Call \`mcp__relai__get_my_tasks\` \u2014 if it returns no tasks, stop immediately.
@@ -301,7 +302,11 @@ Find the default branch with \`git remote show origin\`.
301
302
  \`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
302
303
  the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
303
304
  \`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
304
- 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
305
310
  and independently testable so several agents can work at once. Use \`blockedBy\` only where
306
311
  one task truly needs another's result. Each description is self-contained: what to change,
307
312
  which files, how to test it, and the acceptance criteria.
@@ -562,6 +567,18 @@ async function recordSessionUsage(conn, startIds, usage, logPrefix) {
562
567
  await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
563
568
  }
564
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
+
565
582
  // ../claude-worker/src/session.ts
566
583
  function mcpServerEntry() {
567
584
  return fileURLToPath(new URL("../../mcp-server/src/index.ts", import.meta.url));
@@ -610,7 +627,7 @@ async function runClaudeSession(config2) {
610
627
  "stream-json",
611
628
  "--verbose",
612
629
  "--model",
613
- config2.model,
630
+ pinnedModel() ?? config2.model,
614
631
  "--add-dir",
615
632
  config2.repoPath
616
633
  ];
@@ -731,8 +748,8 @@ function classifySessionError(text) {
731
748
 
732
749
  // ../claude-worker/src/adapters/cursor.ts
733
750
  var bin = () => process.env.CURSOR_BIN ?? "cursor-agent";
734
- function cursorArgs(repoPath, prompt) {
735
- 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];
736
753
  }
737
754
  function readCursorEvent(line) {
738
755
  let event;
@@ -756,7 +773,7 @@ function readCursorEvent(line) {
756
773
  return {};
757
774
  }
758
775
  async function runCursorSession(config2) {
759
- const args = cursorArgs(config2.repoPath, buildPrompt(config2));
776
+ const args = cursorArgs(config2.repoPath, buildPrompt(config2), pinnedModel());
760
777
  console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
761
778
  const taskIdsPromise = listSessionTaskIds(config2, "[cursor-worker]");
762
779
  return new Promise((resolve, reject) => {
@@ -837,10 +854,11 @@ import { unlinkSync as unlinkSync2 } from "fs";
837
854
  var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
838
855
  var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
839
856
  var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
840
- function copilotArgs(repoPath, prompt, mcpConfigPath) {
857
+ function copilotArgs(repoPath, prompt, mcpConfigPath, model) {
841
858
  return [
842
859
  "-p",
843
860
  prompt,
861
+ ...model ? ["--model", model] : [],
844
862
  "--allow-all-tools",
845
863
  "--output-format",
846
864
  "json",
@@ -870,7 +888,7 @@ function readCopilotEvent(line) {
870
888
  }
871
889
  async function runCopilotSession(config2) {
872
890
  const mcpConfigPath = writeMcpConfig(config2, "copilot-worker");
873
- const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath);
891
+ const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath, pinnedModel());
874
892
  console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
875
893
  return new Promise((resolve, reject) => {
876
894
  const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -943,7 +961,7 @@ var bin3 = () => process.env.CODEX_BIN ?? "codex";
943
961
  var codexHome = () => process.env.CODEX_HOME ?? join3(homedir(), ".codex");
944
962
  var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
945
963
  var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
946
- function codexArgs(repoPath, prompt, profile) {
964
+ function codexArgs(repoPath, prompt, profile, model) {
947
965
  return [
948
966
  "exec",
949
967
  "--json",
@@ -954,6 +972,7 @@ function codexArgs(repoPath, prompt, profile) {
954
972
  repoPath,
955
973
  "-p",
956
974
  profile,
975
+ ...model ? ["-m", model] : [],
957
976
  prompt
958
977
  ];
959
978
  }
@@ -1002,7 +1021,7 @@ async function runCodexSession(config2) {
1002
1021
  } catch {
1003
1022
  }
1004
1023
  };
1005
- const args = codexArgs(config2.repoPath, buildPrompt(config2), profile);
1024
+ const args = codexArgs(config2.repoPath, buildPrompt(config2), profile, pinnedModel());
1006
1025
  console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
1007
1026
  return new Promise((resolve, reject) => {
1008
1027
  const proc = spawn4(bin3(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -1287,6 +1306,10 @@ function loadConfig2() {
1287
1306
  };
1288
1307
  }
1289
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
+
1290
1313
  // ../../node_modules/.pnpm/eventsource-parser@3.0.7/node_modules/eventsource-parser/dist/index.js
1291
1314
  var ParseError = class extends Error {
1292
1315
  constructor(message, options) {
@@ -1847,7 +1870,16 @@ async function runEventWorker(config2) {
1847
1870
  }
1848
1871
  try {
1849
1872
  console.log("[event-worker] Event received \u2014 running session...");
1850
- 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
+ }
1851
1883
  await clearUnavailable(config2, "[event-worker]");
1852
1884
  } catch (err) {
1853
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
  }
@@ -12600,7 +12608,7 @@ function buildListPoolsTool(client, repoId, opts) {
12600
12608
  const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
12601
12609
  return {
12602
12610
  name: "list_pools",
12603
- 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,
12604
12612
  inputSchema: external_exports.object({
12605
12613
  repoId: external_exports.string().optional().describe("Repo to read. Defaults to this agent's home repo.")
12606
12614
  }),
@@ -12635,6 +12643,31 @@ function buildSetPoolSizeTool(client, repoId, opts) {
12635
12643
  }
12636
12644
  };
12637
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
+ }
12638
12671
  function resolveAgentRef(candidates, ref, scope) {
12639
12672
  const available = candidates.length ? ` Available: ${candidates.map((a) => `${a.name} (${a.id})`).join(", ")}` : ` ${scope} has no agents.`;
12640
12673
  if (ref.startsWith("agent_")) {
@@ -12713,7 +12746,7 @@ ${version.body}`
12713
12746
  },
12714
12747
  {
12715
12748
  name: "list_agents",
12716
- 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." : ""),
12717
12750
  inputSchema: external_exports.object({}),
12718
12751
  handler: async () => {
12719
12752
  const raw = await client.listAgents();
@@ -12729,6 +12762,9 @@ ${version.body}`
12729
12762
  lastSeenAt: a.lastSeenAt,
12730
12763
  online: typeof a.lastSeenAt === "string" ? now - new Date(a.lastSeenAt).getTime() < ONLINE_WINDOW_MS : false,
12731
12764
  listening: a.listening === true,
12765
+ activity: a.activity ?? null,
12766
+ activitySince: a.activitySince ?? null,
12767
+ idleWithReadyMs24h: typeof a.idleWithReadyMs24h === "number" ? a.idleWithReadyMs24h : 0,
12732
12768
  ...a.id === agentId ? { isYou: true } : {}
12733
12769
  }));
12734
12770
  return {
@@ -12913,10 +12949,10 @@ ${version.body}`
12913
12949
  },
12914
12950
  {
12915
12951
  name: "check_authority",
12916
- 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.",
12917
12953
  inputSchema: external_exports.object({
12918
12954
  id: external_exports.string().describe("The stamp id from metadata.authority or an authority.id field."),
12919
- 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.")
12920
12956
  }),
12921
12957
  handler: async (input) => {
12922
12958
  const authority = await client.checkAuthority(input.id, input.class);
@@ -13053,6 +13089,7 @@ ${version.body}`
13053
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.";
13054
13090
  payload.peerBoundary = PEER_BOUNDARY;
13055
13091
  }
13092
+ if (session.repo?.operations) payload.peerBoundary = PEER_BOUNDARY;
13056
13093
  const orientation = session.orientation;
13057
13094
  const omitted = [
13058
13095
  describeCap("unread messages", session.unreadCount, unread.length, "get_unread_messages"),
@@ -13148,10 +13185,17 @@ ${version.body}`
13148
13185
  inputSchema: external_exports.object({
13149
13186
  taskId: external_exports.string().describe("The ID of the task you are reviewing."),
13150
13187
  decision: external_exports.enum(["approve", "reject"]).describe("Approve or reject."),
13151
- 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
+ )
13152
13192
  }),
13153
13193
  handler: async (input) => {
13154
- 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
+ });
13155
13199
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
13156
13200
  }
13157
13201
  },
@@ -13231,7 +13275,7 @@ ${version.body}`
13231
13275
  ...opts.ownerScoped || opts.role === "orchestrator" ? [buildCreateOrderTool(client, repoId, opts)] : [],
13232
13276
  ...opts.role === "orchestrator" || (opts.tier ?? 0) >= 2 ? [buildUpdateOrderTool(client)] : [],
13233
13277
  buildListPoolsTool(client, repoId, opts),
13234
- ...opts.ownerScoped || opts.role === "orchestrator" ? [buildSetPoolSizeTool(client, repoId, opts)] : [],
13278
+ ...opts.ownerScoped || opts.role === "orchestrator" ? [buildSetPoolSizeTool(client, repoId, opts), buildSetRepoOperationsTool(client, repoId, opts)] : [],
13235
13279
  // Additive, never a swap: an owner-scoped agent is still an agent and needs
13236
13280
  // everything above. buildOperatorTools assumes no agent identity, and its
13237
13281
  // create_task bypasses propose/commit.
@@ -13260,11 +13304,29 @@ async function staleAttentionCount(client, cutoff) {
13260
13304
  const { meta } = await client.getTasksPage({ status: ATTENTION_STATUSES, updatedBefore: cutoff, limit: 1, clip: true });
13261
13305
  return meta?.total ?? 0;
13262
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
+ }
13263
13325
  function buildGodCoordinationTools(client) {
13264
13326
  return [
13265
13327
  {
13266
13328
  name: "list_attention",
13267
- 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. 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.",
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.",
13268
13330
  inputSchema: external_exports.object(attentionWindowSchema),
13269
13331
  handler: async (input) => {
13270
13332
  const cutoff = attentionCutoff(input);
@@ -13282,6 +13344,7 @@ function buildGodCoordinationTools(client) {
13282
13344
  const idle = idleUnroutable ?? [];
13283
13345
  const total = meta?.total ?? tasks.length;
13284
13346
  const notShown = describeCap("tasks", total, tasks.length, "get_task on a specific id");
13347
+ const reviewSilent = await reviewSilentItems(client, tasks);
13285
13348
  return {
13286
13349
  content: [{
13287
13350
  type: "text",
@@ -13289,6 +13352,7 @@ function buildGodCoordinationTools(client) {
13289
13352
  tasks,
13290
13353
  taskCount: total,
13291
13354
  ...idle.length > 0 ? { idleUnroutable: idle } : {},
13355
+ ...reviewSilent.length > 0 ? { reviewSilent } : {},
13292
13356
  ...notShown ? { notShown } : {},
13293
13357
  ...staleCount > 0 ? { staleCount, stale: `${staleCount} older items hidden; pass includeStale: true to see them, and cancel or archive what is dead.` } : {},
13294
13358
  peerBoundary: PEER_BOUNDARY
@@ -13296,6 +13360,22 @@ function buildGodCoordinationTools(client) {
13296
13360
  }]
13297
13361
  };
13298
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
+ }
13299
13379
  }
13300
13380
  ];
13301
13381
  }
@@ -13375,12 +13455,12 @@ function buildProvisioningTools(client, opts = {}) {
13375
13455
  )
13376
13456
  }),
13377
13457
  handler: async (input) => {
13378
- const repo = await client.createRepo({
13458
+ const created = await client.createRepo({
13379
13459
  name: input.name,
13380
13460
  ...input.description ? { description: input.description } : {},
13381
13461
  ...input.repoUrl ? { repoUrl: input.repoUrl } : {}
13382
13462
  });
13383
- return { content: [{ type: "text", text: JSON.stringify({ repo }, null, 2) }] };
13463
+ return { content: [{ type: "text", text: JSON.stringify(created, null, 2) }] };
13384
13464
  }
13385
13465
  },
13386
13466
  // Mints a bearer credential: owner mode and the god credential only.
@@ -13455,7 +13535,7 @@ function buildOperatorTools(client, ownerId) {
13455
13535
  ...buildProvisioningTools(client, { credentialMinting: true }),
13456
13536
  {
13457
13537
  name: "list_agents",
13458
- 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.",
13459
13539
  inputSchema: external_exports.object({
13460
13540
  repoId: external_exports.string().optional().describe("Scope to a specific repo. Omit to list across all your repos.")
13461
13541
  }),
@@ -13471,7 +13551,10 @@ function buildOperatorTools(client, ownerId) {
13471
13551
  repoId: a.repoId,
13472
13552
  lastSeenAt: a.lastSeenAt,
13473
13553
  online: typeof a.lastSeenAt === "string" ? now - new Date(a.lastSeenAt).getTime() < ONLINE_WINDOW_MS : false,
13474
- 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
13475
13558
  }));
13476
13559
  return {
13477
13560
  content: [{
@@ -13561,7 +13644,7 @@ function buildOperatorTools(client, ownerId) {
13561
13644
  },
13562
13645
  {
13563
13646
  name: "list_attention",
13564
- 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.",
13565
13648
  inputSchema: external_exports.object(attentionWindowSchema),
13566
13649
  handler: async (input) => {
13567
13650
  const cutoff = attentionCutoff(input);
@@ -13571,12 +13654,14 @@ function buildOperatorTools(client, ownerId) {
13571
13654
  staleAttentionCount(client, cutoff)
13572
13655
  ]);
13573
13656
  const idle = idleUnroutable ?? [];
13657
+ const reviewSilent = await reviewSilentItems(client, tasks);
13574
13658
  return {
13575
13659
  content: [{
13576
13660
  type: "text",
13577
13661
  text: tasks.length === 0 && idle.length === 0 && staleCount === 0 ? "Nothing needs your attention across your projects right now." : JSON.stringify({
13578
13662
  tasks,
13579
13663
  ...idle.length > 0 ? { idleUnroutable: idle } : {},
13664
+ ...reviewSilent.length > 0 ? { reviewSilent } : {},
13580
13665
  ...staleCount > 0 ? { staleCount, stale: `${staleCount} older items hidden; pass includeStale: true to see them, and cancel or archive what is dead.` } : {}
13581
13666
  }, null, 2)
13582
13667
  }]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitboss/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [