@pitboss/cli 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ On the approval screen you pick:
19
19
  | Runtime | Files written |
20
20
  |---|---|
21
21
  | Claude Code | `.mcp.json` (server `pitboss`), `.claude/settings.local.json` (approves the server and installs the wake-up hooks) |
22
- | Cursor | `.cursor/mcp.json` (server `pitboss`), `.cursor/hooks.json` (stop hook), `.cursor/rules/pitboss.mdc` (wake-up rule) |
22
+ | Cursor | `.cursor/mcp.json` (server `pitboss`), `.cursor/hooks.json` (stop + prompt hooks), `.cursor/rules/pitboss.mdc` (wake-up rule) |
23
23
 
24
24
  `.mcp.json` and `.cursor/mcp.json` hold a secret token. `join` adds every file it writes to `.git/info/exclude`, and refuses to write into a file git already tracks.
25
25
 
@@ -38,6 +38,8 @@ pitboss watch # stream events until Ctrl-C (for a human watching a te
38
38
 
39
39
  `watch --once` reads the agent from the folder's `.mcp.json` or `.cursor/mcp.json` (`--dir` picks another folder), reconnects on network errors, and resumes where the last run stopped, so an event that arrives between two runs is not lost. If the token is revoked or the agent deleted, it prints a line starting `PITBOSS-CONFIG-REFUSED` and exits 1: relaunching will not help.
40
40
 
41
+ After three failed relaunches the hooks go quiet so a refused or offline watcher cannot loop. A human prompt re-arms them; a background-command wake does not.
42
+
41
43
  ## Commands
42
44
 
43
45
  ```bash
package/dist/cli.js CHANGED
@@ -18343,7 +18343,7 @@ async function taskCreateCommand(options) {
18343
18343
  if (options.reviewBy) {
18344
18344
  verifyKind = "reviewer_agent";
18345
18345
  } else if (options.verifyKind) {
18346
- if (!["shell", "file_exists", "thread_concluded", "reviewer_agent"].includes(options.verifyKind)) {
18346
+ if (!["shell", "file_exists", "thread_concluded", "reviewer_agent", "human_approval"].includes(options.verifyKind)) {
18347
18347
  console.error(source_default.red(`Invalid --verify-kind "${options.verifyKind}".`));
18348
18348
  process.exit(1);
18349
18349
  }
@@ -19078,7 +19078,35 @@ function registerWatcher(dir, key) {
19078
19078
 
19079
19079
  // src/lib/runtimes.ts
19080
19080
  import { join as join3 } from "node:path";
19081
- var RUNTIMES = ["claude", "cursor", "windsurf", "gemini", "copilot", "gpt", "mcp"];
19081
+
19082
+ // ../../shared/types/src/index.ts
19083
+ var WORKER_TYPES = ["claude", "copilot", "cursor", "windsurf", "gemini", "gpt", "mcp", "human"];
19084
+ var WORKER_TYPE_LABELS = {
19085
+ claude: "Claude Code",
19086
+ copilot: "Copilot",
19087
+ cursor: "Cursor",
19088
+ windsurf: "Devin Desktop (formerly Windsurf)",
19089
+ gemini: "Gemini",
19090
+ gpt: "GPT",
19091
+ mcp: "Other MCP client",
19092
+ human: "Human"
19093
+ };
19094
+ var RUNTIMES = [
19095
+ { value: "claude", label: WORKER_TYPE_LABELS.claude },
19096
+ { value: "cursor", label: WORKER_TYPE_LABELS.cursor },
19097
+ { value: "gemini", label: WORKER_TYPE_LABELS.gemini },
19098
+ { value: "copilot", label: WORKER_TYPE_LABELS.copilot },
19099
+ { value: "windsurf", label: WORKER_TYPE_LABELS.windsurf },
19100
+ { value: "gpt", label: WORKER_TYPE_LABELS.gpt },
19101
+ { value: "mcp", label: WORKER_TYPE_LABELS.mcp }
19102
+ ];
19103
+ function isWorkerType(value) {
19104
+ return typeof value === "string" && WORKER_TYPES.includes(value);
19105
+ }
19106
+ var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
19107
+
19108
+ // src/lib/runtimes.ts
19109
+ var RUNTIMES2 = RUNTIMES.map((r) => r.value);
19082
19110
  function runtimeTargets(workerType, { home, repo: repo2 }) {
19083
19111
  switch (workerType) {
19084
19112
  case "claude":
@@ -19135,7 +19163,7 @@ function dropLegacyPitbossEntry(config) {
19135
19163
  return { ...config, mcpServers: rest };
19136
19164
  }
19137
19165
  function allRuntimeTargets({ home, repo: repo2 }) {
19138
- const fromRuntimes = RUNTIMES.flatMap((w) => runtimeTargets(w, { home, repo: repo2 }));
19166
+ const fromRuntimes = RUNTIMES2.flatMap((w) => runtimeTargets(w, { home, repo: repo2 }));
19139
19167
  return [.../* @__PURE__ */ new Set([...fromRuntimes, join3(home, ".claude.json")])];
19140
19168
  }
19141
19169
  function holdsRelaiToken(existing, token2) {
@@ -19415,7 +19443,6 @@ function sweepMissingWorkingDirs() {
19415
19443
  }
19416
19444
 
19417
19445
  // src/commands/invite.ts
19418
- var WORKER_TYPES = ["claude", "copilot", "cursor", "windsurf", "gemini", "gpt", "mcp", "human"];
19419
19446
  var SPECIALIZATION_CHOICES = [
19420
19447
  { value: "writer", name: "writer \u2014 implementation, code changes" },
19421
19448
  { value: "reviewer", name: "reviewer \u2014 code review, PR feedback" },
@@ -19502,7 +19529,7 @@ async function loginCommand(opts) {
19502
19529
  process.exit(1);
19503
19530
  }
19504
19531
  if (opts.workerType !== void 0) {
19505
- if (!WORKER_TYPES.includes(opts.workerType)) {
19532
+ if (!isWorkerType(opts.workerType)) {
19506
19533
  console.error(source_default.red(`--worker-type must be one of: ${WORKER_TYPES.join(", ")}`));
19507
19534
  process.exit(1);
19508
19535
  }
@@ -19728,18 +19755,26 @@ function claudeLocalSettings(existing, cli) {
19728
19755
  return {
19729
19756
  ...settings,
19730
19757
  enabledMcpjsonServers: servers.includes("pitboss") ? servers : [...servers, "pitboss"],
19731
- hooks: { ...hooks, SessionStart: withHook("SessionStart"), Stop: withHook("Stop") }
19758
+ hooks: {
19759
+ ...hooks,
19760
+ SessionStart: withHook("SessionStart"),
19761
+ Stop: withHook("Stop"),
19762
+ UserPromptSubmit: withHook("UserPromptSubmit")
19763
+ }
19732
19764
  };
19733
19765
  }
19734
19766
  function cursorHooks(existing, cli, dir) {
19735
19767
  const file = base(existing);
19736
19768
  const hooks = objectField(file, "hooks");
19737
- const stop = Array.isArray(hooks.stop) ? hooks.stop : [];
19738
- const command = `${cli} hook cursor-stop --dir ${shq(dir)}`;
19769
+ const keep = (list) => (Array.isArray(list) ? list : []).filter((h) => !isOurs(h.command));
19739
19770
  return {
19740
19771
  version: 1,
19741
19772
  ...file,
19742
- hooks: { ...hooks, stop: [...stop.filter((h) => !isOurs(h.command)), { command, loop_limit: null }] }
19773
+ hooks: {
19774
+ ...hooks,
19775
+ stop: [...keep(hooks.stop), { command: `${cli} hook cursor-stop --dir ${shq(dir)}`, loop_limit: null }],
19776
+ beforeSubmitPrompt: [...keep(hooks.beforeSubmitPrompt), { command: `${cli} hook cursor-prompt --dir ${shq(dir)}` }]
19777
+ }
19743
19778
  };
19744
19779
  }
19745
19780
  function cursorRule(cli, dir) {
@@ -20252,11 +20287,21 @@ function summarize(e, bodyChars = 80, agentId) {
20252
20287
  return `new thread: "${threadTitle}"`;
20253
20288
  case "thread.concluded":
20254
20289
  return `thread concluded: "${threadTitle}"`;
20290
+ case "thread.message_ceiling": {
20291
+ const count = e.payload.count;
20292
+ const ceiling = e.payload.ceiling;
20293
+ return `message ceiling on thread (${count ?? "?"}/${ceiling ?? "?"})`;
20294
+ }
20255
20295
  case "agent.idle_unroutable": {
20256
20296
  const name = e.payload.agentName ?? e.targetId;
20257
20297
  const n = Array.isArray(e.payload.tasks) ? e.payload.tasks.length : 0;
20258
20298
  return `idle agent ${name} cannot take ${n} ready task(s)`;
20259
20299
  }
20300
+ case "agent.unavailable": {
20301
+ const name = e.payload.agentName ?? e.targetId;
20302
+ const reason = e.payload.reason ?? "quota";
20303
+ return `agent ${name} unavailable (${reason})`;
20304
+ }
20260
20305
  default:
20261
20306
  return `${e.targetType} ${e.targetId}`;
20262
20307
  }
@@ -20276,7 +20321,8 @@ var KIND_STYLE = {
20276
20321
  "task.review_submitted": source_default.magenta,
20277
20322
  "task.review_overdue": source_default.yellow,
20278
20323
  "thread.created": source_default.cyan,
20279
- "thread.concluded": source_default.dim
20324
+ "thread.concluded": source_default.dim,
20325
+ "thread.message_ceiling": source_default.yellow
20280
20326
  };
20281
20327
  function clockOf(iso) {
20282
20328
  const d = new Date(iso);
@@ -20409,6 +20455,30 @@ async function watchCommand(options) {
20409
20455
  import { readFileSync as readFileSync10 } from "node:fs";
20410
20456
  import { join as join11 } from "node:path";
20411
20457
  var FOLLOWUP_CAP = 3;
20458
+ var CURSOR_BACKGROUND_TASK_PROMPT = "Briefly inform the user about the task result";
20459
+ var SYSTEM_REMINDER_ONLY = /^(?:<system-reminder>[\s\S]*?<\/system-reminder>\s*)+$/;
20460
+ function promptFromPayload(payload) {
20461
+ try {
20462
+ const p = JSON.parse(payload).prompt;
20463
+ return typeof p === "string" ? p : null;
20464
+ } catch {
20465
+ return null;
20466
+ }
20467
+ }
20468
+ function isHumanPrompt(prompt) {
20469
+ if (prompt === null) return false;
20470
+ const t = prompt.trim();
20471
+ if (!t) return false;
20472
+ if (t.includes("<task-notification>")) return false;
20473
+ if (SYSTEM_REMINDER_ONLY.test(t)) return false;
20474
+ if (t.includes(CURSOR_BACKGROUND_TASK_PROMPT)) return false;
20475
+ if (t.includes("Your pitboss watcher is not running")) return false;
20476
+ if (t.includes("The pitboss watcher is NOT RUNNING")) return false;
20477
+ return true;
20478
+ }
20479
+ function maybeResetOnPrompt(payload, reset) {
20480
+ if (isHumanPrompt(promptFromPayload(payload))) reset();
20481
+ }
20412
20482
  function wakeInstructions(watch, opening) {
20413
20483
  return `${opening}
20414
20484
 
@@ -20479,11 +20549,16 @@ function wakeStateFor(dir) {
20479
20549
  return {
20480
20550
  watcherAlive: () => watcherAlive(state, key),
20481
20551
  followups: followupCount(state, key),
20482
- asked: () => recordFollowup(state, key)
20552
+ asked: () => recordFollowup(state, key),
20553
+ reset: () => resetFollowups(state, key)
20483
20554
  };
20484
20555
  }
20556
+ function claudeEvent(raw) {
20557
+ if (raw === "Stop" || raw === "UserPromptSubmit") return raw;
20558
+ return "SessionStart";
20559
+ }
20485
20560
  function hookClaudeCommand(opts) {
20486
- const event = opts.event === "Stop" ? "Stop" : "SessionStart";
20561
+ const event = claudeEvent(opts.event);
20487
20562
  const payload = readStdin();
20488
20563
  let cwd;
20489
20564
  try {
@@ -20494,6 +20569,10 @@ function hookClaudeCommand(opts) {
20494
20569
  const dir = process.env.CLAUDE_PROJECT_DIR || typeof cwd === "string" && cwd || process.cwd();
20495
20570
  const wake = wakeStateFor(dir);
20496
20571
  if (!wake) return;
20572
+ if (event === "UserPromptSubmit") {
20573
+ maybeResetOnPrompt(payload, wake.reset);
20574
+ return;
20575
+ }
20497
20576
  const out = claudeHookOutput(event, payload, { cli: pinnedCli(CLI_VERSION), dir, ...wake });
20498
20577
  if (out && event === "Stop") wake.asked();
20499
20578
  if (out) process.stdout.write(JSON.stringify(out));
@@ -20509,6 +20588,12 @@ function hookCursorStopCommand(opts) {
20509
20588
  if ("followup_message" in out) wake.asked();
20510
20589
  process.stdout.write(JSON.stringify(out));
20511
20590
  }
20591
+ function hookCursorPromptCommand(opts) {
20592
+ const dir = opts.dir ?? process.cwd();
20593
+ const wake = wakeStateFor(dir);
20594
+ if (wake) maybeResetOnPrompt(readStdin(), wake.reset);
20595
+ process.stdout.write("{}");
20596
+ }
20512
20597
 
20513
20598
  // src/commands/node.ts
20514
20599
  import { execFileSync as execFileSync6, spawn as spawnChild } from "node:child_process";
@@ -20545,6 +20630,7 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
20545
20630
  with \`toAgent\` set to the sender and no \`threadId\`. To check whether someone answered *you*,
20546
20631
  call \`mcp__relai__get_thread_messages\` on the \`threadId\` \`send_message\` gave you back \u2014 do not
20547
20632
  trawl the unread feed for it.
20633
+ If \`mcp__relai__send_message\` returns 429, batch your updates into one message rather than retrying.
20548
20634
 
20549
20635
  **If you are stuck on something a colleague would know, ask one.** Call
20550
20636
  \`mcp__relai__list_agents\` to see who exists, pick by specialization or domain, prefer someone
@@ -21050,9 +21136,15 @@ import { spawn as spawn3 } from "child_process";
21050
21136
  import { join as join13 } from "path";
21051
21137
 
21052
21138
  // ../claude-worker/src/errors.ts
21053
- var CREDENTIAL_PATTERNS = [
21139
+ var QUOTA_PATTERNS = [
21054
21140
  /credit balance is too low/i,
21055
21141
  /insufficient[ _-]?credits?/i,
21142
+ /quota[ _-]?(?:exceeded|exhausted)/i,
21143
+ /exceeded your (?:usage |rate )?quota/i,
21144
+ /usage limit (?:reached|exceeded)/i,
21145
+ /daily (?:limit|quota|cap)/i
21146
+ ];
21147
+ var CREDENTIAL_PATTERNS = [
21056
21148
  /invalid[ _-]?(api[ _-]?key|x-api-key|bearer token)/i,
21057
21149
  /authentication[ _-]?error/i,
21058
21150
  /\bunauthorized\b/i,
@@ -21068,6 +21160,7 @@ var OVERFLOW_PATTERNS = [
21068
21160
  ];
21069
21161
  function classifySessionError(text) {
21070
21162
  if (!text) return "transient";
21163
+ if (QUOTA_PATTERNS.some((re) => re.test(text))) return "quota";
21071
21164
  if (CREDENTIAL_PATTERNS.some((re) => re.test(text))) return "credentials";
21072
21165
  if (OVERFLOW_PATTERNS.some((re) => re.test(text))) return "overflow";
21073
21166
  return "transient";
@@ -21174,7 +21267,7 @@ var claudeAdapter = {
21174
21267
  };
21175
21268
 
21176
21269
  // ../claude-worker/src/adapters/index.ts
21177
- var RUNTIMES2 = ["claude", "cursor"];
21270
+ var RUNTIMES3 = ["claude", "cursor"];
21178
21271
  var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
21179
21272
  function getAdapter(runtime) {
21180
21273
  const adapter = ADAPTERS[runtime];
@@ -21182,6 +21275,9 @@ function getAdapter(runtime) {
21182
21275
  return adapter;
21183
21276
  }
21184
21277
 
21278
+ // ../claude-worker/src/unavailable.ts
21279
+ var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
21280
+
21185
21281
  // ../claude-worker/src/config.ts
21186
21282
  var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
21187
21283
 
@@ -21208,8 +21304,8 @@ function parseSpec(text, specDir) {
21208
21304
  const a = entry ?? {};
21209
21305
  if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
21210
21306
  if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
21211
- if (!RUNTIMES2.includes(a.runtime)) {
21212
- throw new Error(`agents[${i}]: runtime "${String(a.runtime)}" is not supported (have: ${RUNTIMES2.join(", ")})`);
21307
+ if (!RUNTIMES3.includes(a.runtime)) {
21308
+ throw new Error(`agents[${i}]: runtime "${String(a.runtime)}" is not supported (have: ${RUNTIMES3.join(", ")})`);
21213
21309
  }
21214
21310
  const specialization = a.specialization === void 0 ? "writer" : a.specialization;
21215
21311
  if (!VALID_SPECIALIZATIONS.includes(specialization)) {
@@ -21908,11 +22004,12 @@ program2.command("status").description("Show your agent info, online agents, tas
21908
22004
  program2.command("start").description("Show your session orientation: project context, your open tasks, unread messages, open threads").action(startCommand);
21909
22005
  program2.command("watch").description("Stream live events you're subscribed to (new tasks, messages, reviews) until Ctrl-C").option("--kinds <list>", "Only show these event kinds (comma-separated, e.g. task.created,message.posted)").option("--once", "Block until the next event for this folder's agent, print it as JSON, and exit").option("--dir <path>", "Folder whose agent config --once uses (defaults to the current directory)").action(watchCommand);
21910
22006
  var hook = program2.command("hook", { hidden: true }).description("Agent runtime hooks that keep the watcher running (installed by join)");
21911
- hook.command("claude").description("Claude Code SessionStart/Stop hook").option("--event <event>", "SessionStart | Stop", "SessionStart").action(hookClaudeCommand);
22007
+ hook.command("claude").description("Claude Code SessionStart/Stop/UserPromptSubmit hook").option("--event <event>", "SessionStart | Stop | UserPromptSubmit", "SessionStart").action(hookClaudeCommand);
21912
22008
  hook.command("cursor-stop").description("Cursor stop hook").option("--dir <path>", "The agent's folder").action(hookCursorStopCommand);
22009
+ hook.command("cursor-prompt").description("Cursor beforeSubmitPrompt hook").option("--dir <path>", "The agent's folder").action(hookCursorPromptCommand);
21913
22010
  program2.command("tasks").description("List tasks assigned to you (default: assigned + in_progress)").option("-a, --all", "Show all tasks in the project, not just yours").option("-s, --status <status>", "Filter by status (comma-separated: pending,assigned,in_progress,pending_verification,completed,blocked)").action(tasksCommand);
21914
22011
  var task = program2.command("task").description("Create or update a task");
21915
- task.command("create").description("Create a new task").option("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("-p, --priority <priority>", "low|normal|high|urgent").option("--to <agent>", "Assign to an agent (id or name)").option("--domains <list>", "Comma-separated domain tags").option("--specialization <s>", "Required specialization").option("-v, --verify <cmd>", "Shell command that must exit 0 to gate the `completed` transition").option("--verify-cwd <path>", "Working directory for --verify (defaults to API server cwd)").option("--verify-kind <kind>", "Verifier kind: shell|file_exists|thread_concluded|reviewer_agent").option("--verify-path <path>", "Path that must exist (--verify-kind file_exists)").option("--verify-thread <id>", "Thread that must be concluded (--verify-kind thread_concluded)").option("--verify-reviewer <agent>", "Agent (id or name) who must approve (--verify-kind reviewer_agent)").option("--review-by <agent>", "Shorthand for --verify-kind reviewer_agent --verify-reviewer <agent>").action(taskCreateCommand);
22012
+ task.command("create").description("Create a new task").option("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("-p, --priority <priority>", "low|normal|high|urgent").option("--to <agent>", "Assign to an agent (id or name)").option("--domains <list>", "Comma-separated domain tags").option("--specialization <s>", "Required specialization").option("-v, --verify <cmd>", "Shell command that must exit 0 to gate the `completed` transition").option("--verify-cwd <path>", "Working directory for --verify (defaults to API server cwd)").option("--verify-kind <kind>", "Verifier kind: shell|file_exists|thread_concluded|reviewer_agent|human_approval").option("--verify-path <path>", "Path that must exist (--verify-kind file_exists)").option("--verify-thread <id>", "Thread that must be concluded (--verify-kind thread_concluded)").option("--verify-reviewer <agent>", "Agent (id or name) who must approve (--verify-kind reviewer_agent)").option("--review-by <agent>", "Shorthand for --verify-kind reviewer_agent --verify-reviewer <agent>").action(taskCreateCommand);
21916
22013
  task.command("start <id>").description("Mark a task as in_progress").option("-n, --note <note>", "Optional note to attach").action((id, options) => taskUpdateCommand(id, "in_progress", options));
21917
22014
  task.command("done <id>").description("Mark a task as completed").option("-n, --note <note>", "Optional note to attach").action((id, options) => taskUpdateCommand(id, "completed", options));
21918
22015
  task.command("block <id>").description("Mark a task as blocked").option("-n, --note <note>", "Describe what's blocking you").action((id, options) => taskUpdateCommand(id, "blocked", options));
@@ -21928,7 +22025,7 @@ program2.command("send <threadId>").description("Send a message to a thread").op
21928
22025
  program2.command("inbox").description("Show unread messages").option("-r, --read", "Mark all displayed messages as read").action(inboxCommand);
21929
22026
  program2.command("repos").description("List all repos on this server").action(reposListCommand);
21930
22027
  program2.command("agents").description("List agents in the current repo").action(agentsListCommand);
21931
- program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `pitboss repo invite`").option("--token <token>", "Per-agent token (from the cloud dashboard)").option("--api <url>", "API URL (skips prompt)").option("--working-dir <path>", "Override the working directory (defaults to CWD)").option("--worker-type <type>", "Agent runtime: claude, copilot, cursor, windsurf, gemini, gpt, mcp, human (default: human)").option("--name <name>", "Agent name (defaults to the invite's suggestion when not interactive)").option("--specialization <spec>", "Agent specialization: writer, reviewer, tester, architect, devops, orchestrator (defaults to the invite's suggestion)").action(loginCommand);
22028
+ program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `pitboss repo invite`").option("--token <token>", "Per-agent token (from the cloud dashboard)").option("--api <url>", "API URL (skips prompt)").option("--working-dir <path>", "Override the working directory (defaults to CWD)").option("--worker-type <type>", `Agent runtime: ${WORKER_TYPES.join(", ")} (default: human)`).option("--name <name>", "Agent name (defaults to the invite's suggestion when not interactive)").option("--specialization <spec>", "Agent specialization: writer, reviewer, tester, architect, devops, orchestrator (defaults to the invite's suggestion)").action(loginCommand);
21932
22029
  program2.command("logout").description("Release this directory's local agent claim (does not revoke the server token)").option("--all", "Sweep claims whose working directories no longer exist").action(logoutCommand);
21933
22030
  var repo = program2.command("repo").description("Repo operations");
21934
22031
  repo.command("show [id]").description("Show a repo's details (defaults to the current repo)").action(repoShowCommand);
@@ -26,6 +26,7 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
26
26
  with \`toAgent\` set to the sender and no \`threadId\`. To check whether someone answered *you*,
27
27
  call \`mcp__relai__get_thread_messages\` on the \`threadId\` \`send_message\` gave you back \u2014 do not
28
28
  trawl the unread feed for it.
29
+ If \`mcp__relai__send_message\` returns 429, batch your updates into one message rather than retrying.
29
30
 
30
31
  **If you are stuck on something a colleague would know, ask one.** Call
31
32
  \`mcp__relai__list_agents\` to see who exists, pick by specialization or domain, prefer someone
@@ -531,9 +532,15 @@ import { spawn as spawn2 } from "child_process";
531
532
  import { join as join2 } from "path";
532
533
 
533
534
  // ../claude-worker/src/errors.ts
534
- var CREDENTIAL_PATTERNS = [
535
+ var QUOTA_PATTERNS = [
535
536
  /credit balance is too low/i,
536
537
  /insufficient[ _-]?credits?/i,
538
+ /quota[ _-]?(?:exceeded|exhausted)/i,
539
+ /exceeded your (?:usage |rate )?quota/i,
540
+ /usage limit (?:reached|exceeded)/i,
541
+ /daily (?:limit|quota|cap)/i
542
+ ];
543
+ var CREDENTIAL_PATTERNS = [
537
544
  /invalid[ _-]?(api[ _-]?key|x-api-key|bearer token)/i,
538
545
  /authentication[ _-]?error/i,
539
546
  /\bunauthorized\b/i,
@@ -549,6 +556,7 @@ var OVERFLOW_PATTERNS = [
549
556
  ];
550
557
  function classifySessionError(text) {
551
558
  if (!text) return "transient";
559
+ if (QUOTA_PATTERNS.some((re) => re.test(text))) return "quota";
552
560
  if (CREDENTIAL_PATTERNS.some((re) => re.test(text))) return "credentials";
553
561
  if (OVERFLOW_PATTERNS.some((re) => re.test(text))) return "overflow";
554
562
  return "transient";
@@ -702,6 +710,28 @@ async function blockOverflowedTasks(conn, detail, logPrefix = "[claude-worker]")
702
710
  return blocked;
703
711
  }
704
712
 
713
+ // ../claude-worker/src/unavailable.ts
714
+ var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
715
+ function untilMs() {
716
+ const n = Number(process.env.UNAVAILABLE_UNTIL_MS);
717
+ return Number.isFinite(n) && n >= 1e3 ? n : DEFAULT_UNAVAILABLE_UNTIL_MS;
718
+ }
719
+ async function reportUnavailable(config2, reason, logPrefix = "[claude-worker]") {
720
+ const until = new Date(Date.now() + untilMs()).toISOString();
721
+ await fetch(`${config2.apiUrl}/agents/${config2.agentId}/unavailable`, {
722
+ method: "PUT",
723
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config2.apiSecret}` },
724
+ body: JSON.stringify({ reason: reason.slice(0, 200), until })
725
+ }).catch((err) => console.warn(`${logPrefix} report unavailable failed:`, err.message));
726
+ }
727
+ async function clearUnavailable(config2, logPrefix = "[claude-worker]") {
728
+ await fetch(`${config2.apiUrl}/agents/${config2.agentId}/unavailable`, {
729
+ method: "PUT",
730
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config2.apiSecret}` },
731
+ body: JSON.stringify({ until: null })
732
+ }).catch((err) => console.warn(`${logPrefix} clear unavailable failed:`, err.message));
733
+ }
734
+
705
735
  // ../../shared/git/src/index.ts
706
736
  import { execFileSync } from "node:child_process";
707
737
  function getGitRoot(cwd) {
@@ -1389,7 +1419,8 @@ var EVENT_KINDS = [
1389
1419
  "thread.created",
1390
1420
  "thread.concluded",
1391
1421
  "task.blocked_overdue",
1392
- "artifact.published"
1422
+ "artifact.published",
1423
+ "agent.unavailable"
1393
1424
  ];
1394
1425
  async function runEventWorker(config2) {
1395
1426
  console.log(`[event-worker] Starting \u2014 agent ${config2.agentId}, watching ${config2.apiUrl}/events`);
@@ -1416,6 +1447,7 @@ async function runEventWorker(config2) {
1416
1447
  try {
1417
1448
  console.log("[event-worker] Event received \u2014 running session...");
1418
1449
  await getAdapter(config2.runtime).runSession(config2);
1450
+ await clearUnavailable(config2, "[event-worker]");
1419
1451
  } catch (err) {
1420
1452
  const text = err instanceof Error ? err.message : String(err);
1421
1453
  const failure = getAdapter(config2.runtime).classifyError(text);
@@ -1424,11 +1456,17 @@ async function runEventWorker(config2) {
1424
1456
  console.error(
1425
1457
  blocked.length > 0 ? `[event-worker] Session ran out of context \u2014 blocked ${blocked.join(", ")} pending a split.
1426
1458
  ${text}` : `[event-worker] Session ran out of context but no task was in flight to block.
1459
+ ${text}`
1460
+ );
1461
+ } else if (failure === "quota") {
1462
+ await reportUnavailable(config2, "quota_exhausted", "[event-worker]");
1463
+ console.error(
1464
+ `[event-worker] FATAL error (quota or credit exhausted) \u2014 router will skip this agent until the until-time or a successful session.
1427
1465
  ${text}`
1428
1466
  );
1429
1467
  } else if (failure === "credentials") {
1430
1468
  console.error(
1431
- `[event-worker] FATAL error (likely exhausted credits or bad credentials) \u2014 fix it; the worker keeps listening and will recover on the next event.
1469
+ `[event-worker] FATAL error (likely bad credentials) \u2014 fix it; the worker keeps listening and will recover on the next event.
1432
1470
  ${text}`
1433
1471
  );
1434
1472
  } else {
package/dist/mcp.js CHANGED
@@ -12378,7 +12378,10 @@ var ApiClient = class {
12378
12378
  return this.request("PUT", `/agents/${agentId}/heartbeat`, {});
12379
12379
  }
12380
12380
  getAgent(id) {
12381
- return this.request("GET", `/agents/${id}`);
12381
+ return this.request(
12382
+ "GET",
12383
+ `/agents/${id}`
12384
+ );
12382
12385
  }
12383
12386
  listAgents(repoId) {
12384
12387
  const qs = repoId ? `?repoId=${encodeURIComponent(repoId)}` : "";
@@ -12562,7 +12565,7 @@ ${version.body}`
12562
12565
  },
12563
12566
  {
12564
12567
  name: "create_task",
12565
- description: "Create a new task in this project \u2014 use this to turn a plan into actionable work (e.g. an architect/planner breaking a design into tasks for workers). You are recorded as the task's creator automatically. Assign with assignedTo: an agent ID assigns directly, '@auto' lets the routing scheduler pick (task stays 'pending'), and omitting it means '@auto'. To keep a task yourself, pass your own agent id. Do NOT set status \u2014 it's derived (assigned when there's a concrete assignee, else pending). Optionally add a completion gate via verifyKind: 'reviewer_agent' (set verifyReviewerId \u2014 that agent must approve via submit_review), 'file_exists' (verifyPath), or 'thread_concluded' (verifyThreadId). The 'shell' kind (verifyCommand) is restricted to orchestrator agents and 403s otherwise. Note: if you are a worker (not an orchestrator), this creates a *proposal* \u2014 the task lands in 'proposed' with your assignedTo kept only as a hint, and an orchestrator must commit it before it becomes real work. Orchestrators commit on creation. When creating under an Epic (epicId), the result includes epicSiblings and may include a warning if a sibling already has this title \u2014 that is a warning, not a refusal; the task is still created. A failed create is returned as an error, never as a created-looking payload." + godNote,
12568
+ description: "Create a new task in this project \u2014 use this to turn a plan into actionable work (e.g. an architect/planner breaking a design into tasks for workers). You are recorded as the task's creator automatically. Assign with assignedTo: an agent ID assigns directly, '@auto' lets the routing scheduler pick (task stays 'pending'), and omitting it means '@auto'. To keep a task yourself, pass your own agent id. Do NOT set status \u2014 it's derived (assigned when there's a concrete assignee, else pending). Optionally add a completion gate via verifyKind: 'reviewer_agent' (set verifyReviewerId \u2014 that agent must approve via submit_review), 'file_exists' (verifyPath), 'thread_concluded' (verifyThreadId), or 'human_approval' (the dashboard owner must approve; no agent token can satisfy it). The 'shell' kind (verifyCommand) is restricted to orchestrator agents and 403s otherwise. Note: if you are a worker (not an orchestrator, not tier\u22652), this creates a *proposal* \u2014 the task lands in 'proposed' with your assignedTo kept only as a hint, and an orchestrator must commit it before it becomes real work. Orchestrators and trusted seniors (tier\u22652 only) commit on creation. commit_task stays orchestrator-only. When creating under an Epic (epicId), the result includes epicSiblings and may include a warning if a sibling already has this title \u2014 that is a warning, not a refusal; the task is still created. A failed create is returned as an error, never as a created-looking payload." + godNote,
12566
12569
  inputSchema: external_exports.object({
12567
12570
  ...repoParam,
12568
12571
  title: external_exports.string().min(1).describe("Short, action-oriented task title."),
@@ -12572,7 +12575,7 @@ ${version.body}`
12572
12575
  domains: external_exports.array(external_exports.string()).optional().describe("Domain tags for rules-based routing, e.g. ['database','schema']."),
12573
12576
  specialization: external_exports.string().optional().describe("Specialization the task needs, for routing (e.g. 'writer')."),
12574
12577
  epicId: external_exports.string().optional().describe("Parent Epic (a plan thread). The response lists existing children and warns on a title match."),
12575
- verifyKind: external_exports.enum(["shell", "file_exists", "thread_concluded", "reviewer_agent"]).optional().describe("Optional completion gate \u2014 see tool description."),
12578
+ verifyKind: external_exports.enum(["shell", "file_exists", "thread_concluded", "reviewer_agent", "human_approval"]).optional().describe("Optional completion gate \u2014 see tool description."),
12576
12579
  verifyReviewerId: external_exports.string().optional().describe("For verifyKind='reviewer_agent': agent ID that must approve."),
12577
12580
  verifyThreadId: external_exports.string().optional().describe("For verifyKind='thread_concluded': thread whose conclusion gates completion."),
12578
12581
  verifyPath: external_exports.string().optional().describe("For verifyKind='file_exists': path that must exist."),
@@ -12686,7 +12689,7 @@ ${version.body}`
12686
12689
  },
12687
12690
  {
12688
12691
  name: "send_message",
12689
- description: "Send a message to the orchestrator or another agent. Use the correct type: 'handoff' when you finish a task and the next agent needs context; 'finding' when you discover something that affects other work (not your current task); 'decision' to record an agreed-upon decision that all agents should honor; 'question' when you are blocked and need input before proceeding; 'escalation' when a decision needs human judgment; 'status' for routine progress updates. Be specific in the body \u2014 the receiver has no other context. IMPORTANT: if you were asked something via a relai message or task (toAgent=you, or a task assigned to you), your answer belongs here, posted on that same threadId \u2014 not just written in your own session output. The requester only sees what you post through send_message. To start a conversation with a peer you have no thread with, give toAgent and omit threadId: that opens (or reuses) a private direct thread between the two of you. Find the agent with list_agents. A direct thread is readable only by its two participants.",
12692
+ description: "Send a message to the orchestrator or another agent. Use the correct type: 'handoff' when you finish a task and the next agent needs context; 'finding' when you discover something that affects other work (not your current task); 'decision' to record an agreed-upon decision that all agents should honor; 'question' when you are blocked and need input before proceeding; 'escalation' when a decision needs human judgment; 'status' for routine progress updates. Be specific in the body \u2014 the receiver has no other context. IMPORTANT: if you were asked something via a relai message or task (toAgent=you, or a task assigned to you), your answer belongs here, posted on that same threadId \u2014 not just written in your own session output. The requester only sees what you post through send_message. To start a conversation with a peer you have no thread with, give toAgent and omit threadId: that opens (or reuses) a private direct thread between the two of you. Find the agent with list_agents. A direct thread is readable only by its two participants. If the API returns 429, batch your updates into one message rather than retrying.",
12690
12693
  inputSchema: external_exports.object({
12691
12694
  threadId: external_exports.string().optional().describe(
12692
12695
  "The thread to post to. Use list_threads to find or create one. Omit it (with toAgent set) to direct-message that agent \u2014 no thread needed."
@@ -12983,7 +12986,7 @@ ${version.body}`
12983
12986
  priority: external_exports.enum(["low", "normal", "high", "urgent"]).optional().describe("Ratified priority."),
12984
12987
  domains: external_exports.array(external_exports.string()).optional().describe("Ratified domain tags."),
12985
12988
  specialization: external_exports.string().optional().describe("Ratified specialization."),
12986
- verifyKind: external_exports.enum(["shell", "file_exists", "thread_concluded", "reviewer_agent"]).optional(),
12989
+ verifyKind: external_exports.enum(["shell", "file_exists", "thread_concluded", "reviewer_agent", "human_approval"]).optional(),
12987
12990
  verifyReviewerId: external_exports.string().optional(),
12988
12991
  verifyThreadId: external_exports.string().optional(),
12989
12992
  verifyPath: external_exports.string().optional(),
@@ -13165,7 +13168,7 @@ function buildProvisioningTools(client, opts = {}) {
13165
13168
  inputSchema: external_exports.object({
13166
13169
  repoId: external_exports.string().describe("The repo to invite into. Must be one you own; see list_repos."),
13167
13170
  name: external_exports.string().min(1).describe("Suggested name for the new agent, e.g. 'reviewer'."),
13168
- workerType: external_exports.enum(["claude", "copilot", "cursor", "windsurf", "gemini", "gpt", "mcp", "human"]).optional().describe("The runtime this agent will run as. Defaults to 'human' at redeem time if omitted."),
13171
+ workerType: external_exports.string().optional().describe("The runtime this agent will run as. Defaults to 'human' at redeem time if omitted."),
13169
13172
  specialization: external_exports.string().optional().describe("What this agent is for, e.g. 'review' or 'frontend'. Used for routing.")
13170
13173
  }),
13171
13174
  handler: async (input) => {
@@ -13659,6 +13662,41 @@ async function deliverAttention(prev, tasks, send) {
13659
13662
  return next;
13660
13663
  }
13661
13664
 
13665
+ // ../mcp-server/src/inbox-poll.ts
13666
+ var DEFAULT_INBOX_POLL_INTERVAL_MS = 6e4;
13667
+ function inboxPollIntervalMs(raw = process.env.INBOX_POLL_INTERVAL_MS) {
13668
+ const n = Number(raw);
13669
+ return Number.isFinite(n) && n >= 1 ? n : DEFAULT_INBOX_POLL_INTERVAL_MS;
13670
+ }
13671
+ function shouldSkipInboxPoll(agent) {
13672
+ return agent?.listening === true;
13673
+ }
13674
+ async function runInboxPoll(deps) {
13675
+ let agent;
13676
+ try {
13677
+ agent = await deps.getAgent();
13678
+ } catch {
13679
+ agent = null;
13680
+ }
13681
+ if (shouldSkipInboxPoll(agent)) return "skipped";
13682
+ const [tasks, messages] = await Promise.all([deps.getTasks(), deps.getUnread()]);
13683
+ for (const task of tasks) {
13684
+ if (deps.seenTaskIds.has(task.id)) continue;
13685
+ deps.seenTaskIds.add(task.id);
13686
+ await deps.notify(
13687
+ `\u{1F4CB} New task assigned: "${task.title}" [${task.id}] \u2014 call get_my_tasks to begin`
13688
+ );
13689
+ }
13690
+ for (const msg of messages) {
13691
+ if (deps.seenMessageIds.has(msg.id)) continue;
13692
+ deps.seenMessageIds.add(msg.id);
13693
+ await deps.notify(
13694
+ `\u{1F4AC} New ${msg.type} message from ${msg.fromAgent} in thread ${msg.threadId} \u2014 call get_unread_messages to read`
13695
+ );
13696
+ }
13697
+ return "polled";
13698
+ }
13699
+
13662
13700
  // ../../shared/git/src/index.ts
13663
13701
  import { execFileSync } from "node:child_process";
13664
13702
  function getGitRoot(cwd) {
@@ -13867,31 +13905,25 @@ if (!OWNER_MODE) {
13867
13905
  apiClient.heartbeat(AGENT_ID).catch(() => {
13868
13906
  });
13869
13907
  }, HEARTBEAT_INTERVAL_MS);
13870
- const POLL_INTERVAL_MS = 15e3;
13908
+ const POLL_INTERVAL_MS = inboxPollIntervalMs();
13871
13909
  const seenTaskIds = /* @__PURE__ */ new Set();
13872
13910
  const seenMessageIds = /* @__PURE__ */ new Set();
13873
13911
  async function pollInbox() {
13874
13912
  try {
13875
- const [tasks, messages] = await Promise.all([
13876
- apiClient.getTasks({ repoId: REPO_ID, assignedTo: AGENT_ID, status: "assigned" }),
13877
- apiClient.getUnread(AGENT_ID, REPO_ID).then((r) => r.data ?? [])
13878
- ]);
13879
- const newTasks = tasks.filter((t) => !seenTaskIds.has(t.id));
13880
- const newMessages = messages.filter((m) => !seenMessageIds.has(m.id));
13881
- for (const task of newTasks) {
13882
- seenTaskIds.add(task.id);
13883
- await server.server.sendLoggingMessage({
13884
- level: "info",
13885
- data: `\u{1F4CB} New task assigned: "${task.title}" [${task.id}] \u2014 call get_my_tasks to begin`
13886
- });
13887
- }
13888
- for (const msg of newMessages) {
13889
- seenMessageIds.add(msg.id);
13890
- await server.server.sendLoggingMessage({
13891
- level: "info",
13892
- data: `\u{1F4AC} New ${msg.type} message from ${msg.fromAgent} in thread ${msg.threadId} \u2014 call get_unread_messages to read`
13893
- });
13894
- }
13913
+ await runInboxPoll({
13914
+ getAgent: () => apiClient.getAgent(AGENT_ID),
13915
+ getTasks: () => apiClient.getTasks({
13916
+ repoId: REPO_ID,
13917
+ assignedTo: AGENT_ID,
13918
+ status: "assigned"
13919
+ }),
13920
+ getUnread: () => apiClient.getUnread(AGENT_ID, REPO_ID).then(
13921
+ (r) => r.data ?? []
13922
+ ),
13923
+ seenTaskIds,
13924
+ seenMessageIds,
13925
+ notify: (data) => server.server.sendLoggingMessage({ level: "info", data })
13926
+ });
13895
13927
  } catch (err) {
13896
13928
  console.error("[relai-mcp] inbox poll failed:", err instanceof Error ? err.message : err);
13897
13929
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitboss/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [
@@ -13,14 +13,14 @@
13
13
  "cli"
14
14
  ],
15
15
  "license": "UNLICENSED",
16
- "homepage": "https://github.com/phillipsio/relai/tree/main/packages/cli",
16
+ "homepage": "https://github.com/FunctionizeTeam/pitboss/tree/main/packages/cli",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/phillipsio/relai.git",
19
+ "url": "git+https://github.com/FunctionizeTeam/pitboss.git",
20
20
  "directory": "packages/cli"
21
21
  },
22
22
  "bugs": {
23
- "url": "https://github.com/phillipsio/relai/issues"
23
+ "url": "https://github.com/FunctionizeTeam/pitboss/issues"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"
@@ -44,6 +44,7 @@
44
44
  "devDependencies": {
45
45
  "@getrelai/claude-worker": "workspace:*",
46
46
  "@getrelai/git": "workspace:*",
47
+ "@getrelai/types": "workspace:*",
47
48
  "@inquirer/prompts": "^8.7.0",
48
49
  "@types/node": "^22.0.0",
49
50
  "chalk": "^5.3.0",