@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.
- package/dist/cli.js +656 -241
- package/dist/event-worker.js +107 -11
- package/dist/mcp.js +108 -18
- package/package.json +1 -1
package/dist/event-worker.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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, {
|
|
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.
|
|
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
|
|
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(
|
|
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)
|
|
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
|
|
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
|
}
|